maqcli 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.
Files changed (88) hide show
  1. package/README.md +223 -0
  2. package/dist/core/audit.d.ts +43 -0
  3. package/dist/core/audit.js +77 -0
  4. package/dist/core/board.d.ts +78 -0
  5. package/dist/core/board.js +256 -0
  6. package/dist/core/catalog.d.ts +50 -0
  7. package/dist/core/catalog.js +103 -0
  8. package/dist/core/command-catalog.d.ts +44 -0
  9. package/dist/core/command-catalog.js +86 -0
  10. package/dist/core/completion.d.ts +24 -0
  11. package/dist/core/completion.js +309 -0
  12. package/dist/core/complexity.d.ts +17 -0
  13. package/dist/core/complexity.js +87 -0
  14. package/dist/core/config-store.d.ts +33 -0
  15. package/dist/core/config-store.js +61 -0
  16. package/dist/core/connectivity.d.ts +34 -0
  17. package/dist/core/connectivity.js +49 -0
  18. package/dist/core/cost-tracker.d.ts +89 -0
  19. package/dist/core/cost-tracker.js +189 -0
  20. package/dist/core/cost.d.ts +35 -0
  21. package/dist/core/cost.js +89 -0
  22. package/dist/core/exec.d.ts +43 -0
  23. package/dist/core/exec.js +154 -0
  24. package/dist/core/flows.d.ts +36 -0
  25. package/dist/core/flows.js +96 -0
  26. package/dist/core/headroom.d.ts +36 -0
  27. package/dist/core/headroom.js +88 -0
  28. package/dist/core/help-topics.d.ts +26 -0
  29. package/dist/core/help-topics.js +294 -0
  30. package/dist/core/init-wizard.d.ts +26 -0
  31. package/dist/core/init-wizard.js +168 -0
  32. package/dist/core/interactive-registry.d.ts +50 -0
  33. package/dist/core/interactive-registry.js +86 -0
  34. package/dist/core/interactive.d.ts +48 -0
  35. package/dist/core/interactive.js +137 -0
  36. package/dist/core/logger.d.ts +16 -0
  37. package/dist/core/logger.js +46 -0
  38. package/dist/core/memory.d.ts +28 -0
  39. package/dist/core/memory.js +70 -0
  40. package/dist/core/metered.d.ts +9 -0
  41. package/dist/core/metered.js +16 -0
  42. package/dist/core/model.d.ts +74 -0
  43. package/dist/core/model.js +199 -0
  44. package/dist/core/pipeline.d.ts +33 -0
  45. package/dist/core/pipeline.js +223 -0
  46. package/dist/core/plugins.d.ts +21 -0
  47. package/dist/core/plugins.js +38 -0
  48. package/dist/core/probe.d.ts +48 -0
  49. package/dist/core/probe.js +156 -0
  50. package/dist/core/profiles.d.ts +42 -0
  51. package/dist/core/profiles.js +153 -0
  52. package/dist/core/providers.d.ts +84 -0
  53. package/dist/core/providers.js +275 -0
  54. package/dist/core/recall.d.ts +29 -0
  55. package/dist/core/recall.js +83 -0
  56. package/dist/core/registry.d.ts +41 -0
  57. package/dist/core/registry.js +162 -0
  58. package/dist/core/router.d.ts +33 -0
  59. package/dist/core/router.js +40 -0
  60. package/dist/core/sandbox.d.ts +78 -0
  61. package/dist/core/sandbox.js +268 -0
  62. package/dist/core/session.d.ts +105 -0
  63. package/dist/core/session.js +252 -0
  64. package/dist/core/skills.d.ts +56 -0
  65. package/dist/core/skills.js +289 -0
  66. package/dist/core/subagent.d.ts +40 -0
  67. package/dist/core/subagent.js +55 -0
  68. package/dist/core/supervisor.d.ts +37 -0
  69. package/dist/core/supervisor.js +40 -0
  70. package/dist/core/tools.d.ts +39 -0
  71. package/dist/core/tools.js +159 -0
  72. package/dist/core/types.d.ts +87 -0
  73. package/dist/core/types.js +10 -0
  74. package/dist/index.d.ts +11 -0
  75. package/dist/index.js +1032 -0
  76. package/dist/phases/execute.d.ts +39 -0
  77. package/dist/phases/execute.js +166 -0
  78. package/dist/phases/plan.d.ts +11 -0
  79. package/dist/phases/plan.js +118 -0
  80. package/dist/phases/scout.d.ts +10 -0
  81. package/dist/phases/scout.js +113 -0
  82. package/dist/phases/verify.d.ts +22 -0
  83. package/dist/phases/verify.js +81 -0
  84. package/dist/server/daemon.d.ts +50 -0
  85. package/dist/server/daemon.js +377 -0
  86. package/dist/server/relay-bridge.d.ts +44 -0
  87. package/dist/server/relay-bridge.js +175 -0
  88. package/package.json +39 -0
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Interactive worker — the "steer a running worker" capability (CAO's live-PTY
3
+ * value) WITHOUT a mandatory native PTY dependency. It spawns a worker with a
4
+ * kept-open stdin pipe, streams stdout/stderr line-by-line as normalized
5
+ * events, infers a live status (starting → processing ⇄ idle → exited), and
6
+ * lets a caller `write()` follow-up input mid-task (inbox → stdin steering).
7
+ *
8
+ * Zero runtime deps (node:child_process). A full terminal-emulation PTY backend
9
+ * (node-pty) can be added later as an opt-in for CLIs that require a real TTY;
10
+ * this covers CLIs that read line-based stdin.
11
+ */
12
+ import { spawn } from "node:child_process";
13
+ import { makeEvent } from "./types.js";
14
+ import { normalizeLine } from "../phases/execute.js";
15
+ /** Heuristic: worker output that indicates the CLI needs the user to (re)login. */
16
+ const LOGIN_RE = /\b(not logged in|please (log|sign)\s?in|login required|authenticate|unauthorized|session expired|invalid api key|run .*login|401 )\b/i;
17
+ export class InteractiveWorker {
18
+ bin;
19
+ args;
20
+ child = null;
21
+ _status = "starting";
22
+ idleTimer = null;
23
+ outBuf = "";
24
+ errBuf = "";
25
+ idleMs;
26
+ opts;
27
+ exitResolve = null;
28
+ constructor(bin, args = [], opts = {}) {
29
+ this.bin = bin;
30
+ this.args = args;
31
+ this.opts = opts;
32
+ this.idleMs = opts.idleMs ?? 800;
33
+ }
34
+ get status() {
35
+ return this._status;
36
+ }
37
+ start() {
38
+ const child = spawn(this.bin, this.args, {
39
+ cwd: this.opts.cwd ?? process.cwd(),
40
+ env: this.opts.env ?? process.env,
41
+ shell: false,
42
+ windowsHide: true,
43
+ });
44
+ this.child = child;
45
+ this.setStatus("starting");
46
+ child.stdout.on("data", (d) => this.pump(d.toString(), "stdout"));
47
+ child.stderr.on("data", (d) => this.pump(d.toString(), "stderr"));
48
+ child.on("error", (e) => {
49
+ this.emit(makeEvent("agent.stderr", { text: String(e) }));
50
+ this.setStatus("exited");
51
+ });
52
+ child.on("close", (code) => {
53
+ if (this.outBuf)
54
+ this.emitLine(this.outBuf, "stdout");
55
+ if (this.errBuf)
56
+ this.emitLine(this.errBuf, "stderr");
57
+ if (this.idleTimer)
58
+ clearTimeout(this.idleTimer);
59
+ this.setStatus("exited");
60
+ this.exitResolve?.(code);
61
+ });
62
+ }
63
+ /** Steer the worker: send a line of input to its stdin. */
64
+ write(text) {
65
+ if (!this.child || this._status === "exited")
66
+ return false;
67
+ try {
68
+ this.child.stdin.write(text.endsWith("\n") ? text : text + "\n");
69
+ this.setStatus("processing");
70
+ this.armIdle();
71
+ return true;
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ /** Signal end-of-input (close stdin). */
78
+ end() {
79
+ try {
80
+ this.child?.stdin.end();
81
+ }
82
+ catch {
83
+ /* ignore */
84
+ }
85
+ }
86
+ kill() {
87
+ try {
88
+ this.child?.kill("SIGKILL");
89
+ }
90
+ catch {
91
+ /* ignore */
92
+ }
93
+ }
94
+ /** Resolve when the process exits. */
95
+ wait() {
96
+ if (this._status === "exited")
97
+ return Promise.resolve(null);
98
+ return new Promise((resolve) => (this.exitResolve = resolve));
99
+ }
100
+ pump(chunk, which) {
101
+ this.setStatus("processing");
102
+ let buf = (which === "stdout" ? this.outBuf : this.errBuf) + chunk;
103
+ const parts = buf.split(/\r?\n/);
104
+ buf = parts.pop() ?? "";
105
+ if (which === "stdout")
106
+ this.outBuf = buf;
107
+ else
108
+ this.errBuf = buf;
109
+ for (const line of parts)
110
+ this.emitLine(line, which);
111
+ this.armIdle();
112
+ }
113
+ emitLine(line, which) {
114
+ if (LOGIN_RE.test(line)) {
115
+ this.emit(makeEvent("agent.event", { alert: "needs-login", hint: line.slice(0, 200) }));
116
+ }
117
+ this.emit(normalizeLine({ stream: which, line }));
118
+ }
119
+ armIdle() {
120
+ if (this.idleTimer)
121
+ clearTimeout(this.idleTimer);
122
+ this.idleTimer = setTimeout(() => {
123
+ if (this._status === "processing")
124
+ this.setStatus("idle");
125
+ }, this.idleMs);
126
+ }
127
+ setStatus(s) {
128
+ if (this._status === s)
129
+ return;
130
+ this._status = s;
131
+ this.opts.onStatus?.(s);
132
+ this.emit(makeEvent("agent.event", { status: s }));
133
+ }
134
+ emit(e) {
135
+ this.opts.onEvent?.(e);
136
+ }
137
+ }
@@ -0,0 +1,16 @@
1
+ /** Minimal, dependency-free logger with level control and a quiet/JSON mode. */
2
+ export type LogLevel = "debug" | "info" | "warn" | "error" | "silent";
3
+ export declare class Logger {
4
+ private level;
5
+ private json;
6
+ constructor(level?: LogLevel, json?: boolean);
7
+ setLevel(level: LogLevel): void;
8
+ private emit;
9
+ debug(msg: string, extra?: Record<string, unknown>): void;
10
+ info(msg: string, extra?: Record<string, unknown>): void;
11
+ warn(msg: string, extra?: Record<string, unknown>): void;
12
+ error(msg: string, extra?: Record<string, unknown>): void;
13
+ /** Structured, user-facing output goes to stdout (so it can be piped). */
14
+ out(line: string): void;
15
+ }
16
+ export declare const logger: Logger;
@@ -0,0 +1,46 @@
1
+ /** Minimal, dependency-free logger with level control and a quiet/JSON mode. */
2
+ const LEVELS = {
3
+ debug: 10,
4
+ info: 20,
5
+ warn: 30,
6
+ error: 40,
7
+ silent: 99,
8
+ };
9
+ export class Logger {
10
+ level;
11
+ json;
12
+ constructor(level = "info", json = false) {
13
+ this.level = LEVELS[level];
14
+ this.json = json;
15
+ }
16
+ setLevel(level) {
17
+ this.level = LEVELS[level];
18
+ }
19
+ emit(level, msg, extra) {
20
+ if (LEVELS[level] < this.level)
21
+ return;
22
+ if (this.json) {
23
+ process.stderr.write(JSON.stringify({ level, msg, ...extra }) + "\n");
24
+ return;
25
+ }
26
+ const prefix = level === "error" ? "x" : level === "warn" ? "!" : level === "debug" ? "." : "-";
27
+ process.stderr.write(`${prefix} ${msg}\n`);
28
+ }
29
+ debug(msg, extra) {
30
+ this.emit("debug", msg, extra);
31
+ }
32
+ info(msg, extra) {
33
+ this.emit("info", msg, extra);
34
+ }
35
+ warn(msg, extra) {
36
+ this.emit("warn", msg, extra);
37
+ }
38
+ error(msg, extra) {
39
+ this.emit("error", msg, extra);
40
+ }
41
+ /** Structured, user-facing output goes to stdout (so it can be piped). */
42
+ out(line) {
43
+ process.stdout.write(line + "\n");
44
+ }
45
+ }
46
+ export const logger = new Logger();
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Self-learning memory loop.
3
+ *
4
+ * When Verify fails, we append a short, structured lesson to the project's
5
+ * AGENTS.md (the convention worker CLIs read for standing instructions). Over
6
+ * repeated runs this becomes corrective guidance the worker sees up front, so
7
+ * the same failure is less likely to recur — the "headroom learn" idea from the
8
+ * product docs, implemented locally and dependency-free.
9
+ *
10
+ * It is intentionally conservative: it only ever appends, caps file growth, and
11
+ * never rewrites existing content.
12
+ */
13
+ import type { PipelineResult } from "./types.js";
14
+ export interface LessonInput {
15
+ task: string;
16
+ method: string;
17
+ details: string;
18
+ errors?: string[];
19
+ }
20
+ /** Format a single lesson block. */
21
+ export declare function formatLesson(input: LessonInput): string;
22
+ /**
23
+ * Append a lesson to AGENTS.md in `cwd`. Returns the path written, or null if
24
+ * skipped (e.g. file already at the size cap). Never throws.
25
+ */
26
+ export declare function recordLesson(cwd: string, input: LessonInput): string | null;
27
+ /** Convenience: learn from a completed pipeline result when it failed verify. */
28
+ export declare function learnFromResult(cwd: string, result: PipelineResult): string | null;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Self-learning memory loop.
3
+ *
4
+ * When Verify fails, we append a short, structured lesson to the project's
5
+ * AGENTS.md (the convention worker CLIs read for standing instructions). Over
6
+ * repeated runs this becomes corrective guidance the worker sees up front, so
7
+ * the same failure is less likely to recur — the "headroom learn" idea from the
8
+ * product docs, implemented locally and dependency-free.
9
+ *
10
+ * It is intentionally conservative: it only ever appends, caps file growth, and
11
+ * never rewrites existing content.
12
+ */
13
+ import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ const HEADER = "# AGENTS.md\n\nStanding notes for worker CLIs. MAQ appends lessons from failed verifications below.\n";
16
+ const SECTION = "\n## MAQ — lessons from verification failures\n";
17
+ const MAX_BYTES = 64 * 1024;
18
+ /** Format a single lesson block. */
19
+ export function formatLesson(input) {
20
+ const ts = new Date().toISOString();
21
+ const errs = (input.errors ?? []).filter(Boolean).slice(0, 3);
22
+ const lines = [
23
+ `\n### ${ts}`,
24
+ `- Task: ${clip(input.task, 200)}`,
25
+ `- Verification: ${clip(input.method, 120)} — ${clip(input.details, 300)}`,
26
+ ];
27
+ if (errs.length)
28
+ lines.push(`- Errors: ${errs.map((e) => clip(e, 200)).join(" | ")}`);
29
+ lines.push(`- Rule: re-check this path next time; add/adjust a test that would have caught it.`);
30
+ return lines.join("\n") + "\n";
31
+ }
32
+ /**
33
+ * Append a lesson to AGENTS.md in `cwd`. Returns the path written, or null if
34
+ * skipped (e.g. file already at the size cap). Never throws.
35
+ */
36
+ export function recordLesson(cwd, input) {
37
+ try {
38
+ const path = join(cwd, "AGENTS.md");
39
+ if (!existsSync(path)) {
40
+ writeFileSync(path, HEADER + SECTION, "utf8");
41
+ }
42
+ else {
43
+ const cur = readFileSync(path, "utf8");
44
+ if (cur.length > MAX_BYTES)
45
+ return null;
46
+ if (!cur.includes(SECTION.trim()))
47
+ appendFileSync(path, SECTION, "utf8");
48
+ }
49
+ appendFileSync(path, formatLesson(input), "utf8");
50
+ return path;
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ /** Convenience: learn from a completed pipeline result when it failed verify. */
57
+ export function learnFromResult(cwd, result) {
58
+ if (result.verify.verified)
59
+ return null;
60
+ return recordLesson(cwd, {
61
+ task: result.task,
62
+ method: result.verify.method,
63
+ details: result.verify.details,
64
+ errors: result.execute.errors,
65
+ });
66
+ }
67
+ function clip(s, n) {
68
+ const t = (s ?? "").replace(/\s+/g, " ").trim();
69
+ return t.length > n ? t.slice(0, n) + "…" : t;
70
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Metering wrapper — records every master model call into a CostMeter without
3
+ * changing any caller. Wrap a provider before handing it to a phase and the
4
+ * pipeline gets end-to-end token/cost visibility (the "$0 / token-efficiency"
5
+ * story, made measurable rather than assumed).
6
+ */
7
+ import type { ModelProvider } from "./model.js";
8
+ import type { CostMeter } from "./cost.js";
9
+ export declare function meteredProvider(inner: ModelProvider, meter: CostMeter): ModelProvider;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Metering wrapper — records every master model call into a CostMeter without
3
+ * changing any caller. Wrap a provider before handing it to a phase and the
4
+ * pipeline gets end-to-end token/cost visibility (the "$0 / token-efficiency"
5
+ * story, made measurable rather than assumed).
6
+ */
7
+ export function meteredProvider(inner, meter) {
8
+ return {
9
+ name: inner.name,
10
+ async complete(req) {
11
+ const res = await inner.complete(req);
12
+ meter.record(res.model, res.promptTokensEst, res.completionTokensEst);
13
+ return res;
14
+ },
15
+ };
16
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Model provider abstraction (LiteLLM-style single interface).
3
+ *
4
+ * All master-model calls (Scout summaries, Plan candidate generation, Verify
5
+ * review) go through one interface so the provider/model is swappable and cost
6
+ * is trackable. A "heuristic" provider is included so the whole pipeline runs
7
+ * fully offline with no API key — this is what makes the CLI verifiable and
8
+ * usable at $0, and doubles as the deterministic fallback.
9
+ */
10
+ export type Role = "system" | "user" | "assistant";
11
+ export interface Message {
12
+ role: Role;
13
+ content: string;
14
+ }
15
+ export interface CompletionRequest {
16
+ model: string;
17
+ messages: Message[];
18
+ maxTokens?: number;
19
+ /** Free-form tier hint the router can use: "cheap" | "strong". */
20
+ tier?: "cheap" | "strong";
21
+ }
22
+ export interface CompletionResponse {
23
+ text: string;
24
+ model: string;
25
+ provider: string;
26
+ promptTokensEst: number;
27
+ completionTokensEst: number;
28
+ /** Estimated USD cost for this call (0 for local/offline providers). */
29
+ costUsd?: number;
30
+ }
31
+ export interface ModelProvider {
32
+ readonly name: string;
33
+ complete(req: CompletionRequest): Promise<CompletionResponse>;
34
+ }
35
+ /**
36
+ * Deterministic, offline provider. It does not call any network. It produces
37
+ * structured, useful-enough output for the pipeline by pattern-matching on a
38
+ * lightweight instruction convention embedded in the system message:
39
+ * - "MODE:plan" -> emit 2-3 candidate approaches as a JSON array
40
+ * - "MODE:verify" -> emit a pass/fail verdict as JSON
41
+ * - otherwise -> emit a concise summary of the user content
42
+ */
43
+ export declare class HeuristicProvider implements ModelProvider {
44
+ readonly name = "heuristic";
45
+ complete(req: CompletionRequest): Promise<CompletionResponse>;
46
+ private summarize;
47
+ private planCandidates;
48
+ private verify;
49
+ }
50
+ /**
51
+ * Factory: returns a provider for the configured provider name.
52
+ *
53
+ * Recognized names (case-insensitive):
54
+ * heuristic -> offline deterministic provider (no network, $0)
55
+ * openai -> OpenAI (OPENAI_API_KEY, OPENAI_BASE_URL?)
56
+ * anthropic -> Anthropic Messages API (ANTHROPIC_API_KEY)
57
+ * ollama -> local Ollama native API (OLLAMA_HOST?)
58
+ * groq -> Groq OpenAI-compatible (GROQ_API_KEY)
59
+ * openai-compatible |
60
+ * litellm -> any OpenAI-compatible endpoint
61
+ * (MAQ_PROVIDER_BASE_URL, MAQ_PROVIDER_API_KEY)
62
+ *
63
+ * Missing credentials fall back to the heuristic provider so the pipeline
64
+ * always runs. Pass `strict: true` to throw instead (used by `maq models`).
65
+ */
66
+ export declare function getProvider(name: string, opts?: {
67
+ strict?: boolean;
68
+ }): ModelProvider;
69
+ /**
70
+ * RouteLLM-style tier hint: cheap for triage/scout/verify, strong for the hard
71
+ * plan/exec step. Kept trivial and deterministic here; a trained router slots
72
+ * in behind this signature later.
73
+ */
74
+ export declare function routeTier(complexity: "trivial" | "standard" | "complex"): "cheap" | "strong";
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Model provider abstraction (LiteLLM-style single interface).
3
+ *
4
+ * All master-model calls (Scout summaries, Plan candidate generation, Verify
5
+ * review) go through one interface so the provider/model is swappable and cost
6
+ * is trackable. A "heuristic" provider is included so the whole pipeline runs
7
+ * fully offline with no API key — this is what makes the CLI verifiable and
8
+ * usable at $0, and doubles as the deterministic fallback.
9
+ */
10
+ import { OpenAICompatibleProvider, AnthropicProvider, OllamaProvider, CliProvider } from "./providers.js";
11
+ import { detectAgents, agentSpec } from "./registry.js";
12
+ import { execSafe } from "./exec.js";
13
+ function estTokens(s) {
14
+ return Math.ceil(s.length / 4);
15
+ }
16
+ /**
17
+ * Deterministic, offline provider. It does not call any network. It produces
18
+ * structured, useful-enough output for the pipeline by pattern-matching on a
19
+ * lightweight instruction convention embedded in the system message:
20
+ * - "MODE:plan" -> emit 2-3 candidate approaches as a JSON array
21
+ * - "MODE:verify" -> emit a pass/fail verdict as JSON
22
+ * - otherwise -> emit a concise summary of the user content
23
+ */
24
+ export class HeuristicProvider {
25
+ name = "heuristic";
26
+ async complete(req) {
27
+ const sys = req.messages.find((m) => m.role === "system")?.content ?? "";
28
+ const user = req.messages.filter((m) => m.role === "user").map((m) => m.content).join("\n");
29
+ let text;
30
+ if (sys.includes("MODE:plan")) {
31
+ text = JSON.stringify(this.planCandidates(user));
32
+ }
33
+ else if (sys.includes("MODE:verify")) {
34
+ text = JSON.stringify(this.verify(user));
35
+ }
36
+ else {
37
+ text = this.summarize(user);
38
+ }
39
+ const prompt = req.messages.map((m) => m.content).join("\n");
40
+ return {
41
+ text,
42
+ model: req.model,
43
+ provider: this.name,
44
+ promptTokensEst: estTokens(prompt),
45
+ completionTokensEst: estTokens(text),
46
+ };
47
+ }
48
+ summarize(text) {
49
+ const firstLine = text.split(/\r?\n/).find((l) => l.trim().length > 0) ?? "";
50
+ return firstLine.slice(0, 240).trim();
51
+ }
52
+ planCandidates(task) {
53
+ // The plan prompt embeds several lines; pull out the actual task line.
54
+ const taskLine = task.split(/\r?\n/).find((l) => /^task:/i.test(l.trim()))?.replace(/^task:\s*/i, "") ?? task;
55
+ const t = taskLine.replace(/\s+/g, " ").trim();
56
+ return [
57
+ {
58
+ summary: `Direct approach: implement "${t.slice(0, 80)}" with the smallest change set`,
59
+ steps: [
60
+ "Identify the exact file(s) to change from scout findings",
61
+ "Apply the minimal change",
62
+ "Run the project's tests",
63
+ ],
64
+ },
65
+ {
66
+ summary: `Isolated approach: implement via a new, separately-testable module`,
67
+ steps: [
68
+ "Create a new module for the change",
69
+ "Wire it into the existing entry point",
70
+ "Add focused tests, then run the suite",
71
+ ],
72
+ },
73
+ {
74
+ summary: `Refactor-first approach: clean the surrounding seam, then implement`,
75
+ steps: [
76
+ "Extract the affected logic to a clear boundary",
77
+ "Implement the change against the new boundary",
78
+ "Run tests and lint",
79
+ ],
80
+ },
81
+ ];
82
+ }
83
+ verify(diffOrLog) {
84
+ const s = diffOrLog.toLowerCase();
85
+ const failed = /\b(error|failed|exception|traceback|not ok|fatal)\b/.test(s);
86
+ return failed
87
+ ? { pass: false, reason: "output contains failure signals" }
88
+ : { pass: true, reason: "no failure signals detected in output" };
89
+ }
90
+ }
91
+ /**
92
+ * Factory: returns a provider for the configured provider name.
93
+ *
94
+ * Recognized names (case-insensitive):
95
+ * heuristic -> offline deterministic provider (no network, $0)
96
+ * openai -> OpenAI (OPENAI_API_KEY, OPENAI_BASE_URL?)
97
+ * anthropic -> Anthropic Messages API (ANTHROPIC_API_KEY)
98
+ * ollama -> local Ollama native API (OLLAMA_HOST?)
99
+ * groq -> Groq OpenAI-compatible (GROQ_API_KEY)
100
+ * openai-compatible |
101
+ * litellm -> any OpenAI-compatible endpoint
102
+ * (MAQ_PROVIDER_BASE_URL, MAQ_PROVIDER_API_KEY)
103
+ *
104
+ * Missing credentials fall back to the heuristic provider so the pipeline
105
+ * always runs. Pass `strict: true` to throw instead (used by `maq models`).
106
+ */
107
+ export function getProvider(name, opts = {}) {
108
+ const key = (name || "heuristic").toLowerCase();
109
+ const timeoutMs = envNum("MAQ_MODEL_TIMEOUT_MS", 60000);
110
+ const maxRetries = envNum("MAQ_MODEL_RETRIES", 3);
111
+ // CLI-as-provider: "cli:<agent>" reuses an authenticated worker CLI as the
112
+ // master's model at $0 marginal cost.
113
+ if (key.startsWith("cli")) {
114
+ const agentName = key.includes(":") ? key.slice(key.indexOf(":") + 1) : "";
115
+ const spec = agentSpec(agentName);
116
+ const agents = detectAgents();
117
+ const found = agents.find((a) => a.name === agentName);
118
+ if (!agentName || !spec || !spec.headless || !found?.binPath) {
119
+ return fallback(key, `CLI agent '${agentName}' not available/authenticated`, opts.strict);
120
+ }
121
+ const runner = async (bin, args) => {
122
+ const o = await execSafe(bin, args, { timeoutMs });
123
+ return { code: o.code, stdout: o.stdout, stderr: o.stderr };
124
+ };
125
+ return new CliProvider(agentName, found.binPath, spec.headless, runner, timeoutMs);
126
+ }
127
+ switch (key) {
128
+ case "heuristic":
129
+ case "offline":
130
+ return new HeuristicProvider();
131
+ case "openai": {
132
+ const apiKey = process.env.OPENAI_API_KEY;
133
+ if (!apiKey)
134
+ return fallback("openai", "OPENAI_API_KEY not set", opts.strict);
135
+ return new OpenAICompatibleProvider("openai", {
136
+ baseUrl: process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1",
137
+ apiKey,
138
+ timeoutMs,
139
+ maxRetries,
140
+ });
141
+ }
142
+ case "anthropic": {
143
+ const apiKey = process.env.ANTHROPIC_API_KEY;
144
+ if (!apiKey)
145
+ return fallback("anthropic", "ANTHROPIC_API_KEY not set", opts.strict);
146
+ return new AnthropicProvider({
147
+ baseUrl: process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com/v1",
148
+ apiKey,
149
+ timeoutMs,
150
+ maxRetries,
151
+ });
152
+ }
153
+ case "groq": {
154
+ const apiKey = process.env.GROQ_API_KEY;
155
+ if (!apiKey)
156
+ return fallback("groq", "GROQ_API_KEY not set", opts.strict);
157
+ return new OpenAICompatibleProvider("groq", {
158
+ baseUrl: process.env.GROQ_BASE_URL ?? "https://api.groq.com/openai/v1",
159
+ apiKey,
160
+ timeoutMs,
161
+ maxRetries,
162
+ });
163
+ }
164
+ case "ollama":
165
+ return new OllamaProvider({ timeoutMs: envNum("MAQ_MODEL_TIMEOUT_MS", 120000), maxRetries });
166
+ case "openai-compatible":
167
+ case "litellm":
168
+ case "proxy": {
169
+ const baseUrl = process.env.MAQ_PROVIDER_BASE_URL ?? process.env.LITELLM_BASE_URL;
170
+ if (!baseUrl)
171
+ return fallback(key, "MAQ_PROVIDER_BASE_URL not set", opts.strict);
172
+ return new OpenAICompatibleProvider(key, {
173
+ baseUrl,
174
+ apiKey: process.env.MAQ_PROVIDER_API_KEY ?? process.env.LITELLM_API_KEY,
175
+ timeoutMs,
176
+ maxRetries,
177
+ });
178
+ }
179
+ default:
180
+ return fallback(key, `unknown provider '${name}'`, opts.strict);
181
+ }
182
+ }
183
+ function fallback(name, reason, strict) {
184
+ if (strict)
185
+ throw new Error(`provider '${name}' unavailable: ${reason}`);
186
+ return new HeuristicProvider();
187
+ }
188
+ function envNum(name, dflt) {
189
+ const v = Number(process.env[name]);
190
+ return Number.isFinite(v) && v > 0 ? v : dflt;
191
+ }
192
+ /**
193
+ * RouteLLM-style tier hint: cheap for triage/scout/verify, strong for the hard
194
+ * plan/exec step. Kept trivial and deterministic here; a trained router slots
195
+ * in behind this signature later.
196
+ */
197
+ export function routeTier(complexity) {
198
+ return complexity === "complex" ? "strong" : "cheap";
199
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Pipeline orchestrator: Scout -> Plan -> Execute -> Verify.
3
+ *
4
+ * The cost/quality dial lives here: trivial tasks skip Scout+Plan and go
5
+ * straight to a minimal Execute; standard/complex tasks run the full pipeline.
6
+ * Every phase transition emits a normalized event so a UI can render progress
7
+ * uniformly regardless of which worker CLI runs underneath.
8
+ */
9
+ import type { MaqEvent, PipelineResult } from "./types.js";
10
+ import { agentSpec } from "./registry.js";
11
+ export interface PipelineOptions {
12
+ cwd?: string;
13
+ target?: string;
14
+ dryRun?: boolean;
15
+ /** Sink for progress events (defaults to collecting only). */
16
+ onEvent?: (e: MaqEvent) => void;
17
+ timeoutMs?: number;
18
+ /** Abort signal; when aborted, in-flight worker processes are killed. */
19
+ signal?: AbortSignal;
20
+ /**
21
+ * Cooperative control checkpoint, awaited at every phase boundary. It should
22
+ * reject (to abort the run) when cancelled, and stay pending while paused.
23
+ */
24
+ checkpoint?: () => Promise<void>;
25
+ /** Ad-hoc provider override (else config.provider). */
26
+ provider?: string;
27
+ /** Ad-hoc model override applied to both tiers (else config tier models). */
28
+ model?: string;
29
+ /** Write run artifacts + a hash-chained audit log under .maq/runs/<id>. */
30
+ audit?: boolean;
31
+ }
32
+ export declare function runPipeline(task: string, opts?: PipelineOptions): Promise<PipelineResult>;
33
+ export { agentSpec };