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.
- package/README.md +223 -0
- package/dist/core/audit.d.ts +43 -0
- package/dist/core/audit.js +77 -0
- package/dist/core/board.d.ts +78 -0
- package/dist/core/board.js +256 -0
- package/dist/core/catalog.d.ts +50 -0
- package/dist/core/catalog.js +103 -0
- package/dist/core/command-catalog.d.ts +44 -0
- package/dist/core/command-catalog.js +86 -0
- package/dist/core/completion.d.ts +24 -0
- package/dist/core/completion.js +309 -0
- package/dist/core/complexity.d.ts +17 -0
- package/dist/core/complexity.js +87 -0
- package/dist/core/config-store.d.ts +33 -0
- package/dist/core/config-store.js +61 -0
- package/dist/core/connectivity.d.ts +34 -0
- package/dist/core/connectivity.js +49 -0
- package/dist/core/cost-tracker.d.ts +89 -0
- package/dist/core/cost-tracker.js +189 -0
- package/dist/core/cost.d.ts +35 -0
- package/dist/core/cost.js +89 -0
- package/dist/core/exec.d.ts +43 -0
- package/dist/core/exec.js +154 -0
- package/dist/core/flows.d.ts +36 -0
- package/dist/core/flows.js +96 -0
- package/dist/core/headroom.d.ts +36 -0
- package/dist/core/headroom.js +88 -0
- package/dist/core/help-topics.d.ts +26 -0
- package/dist/core/help-topics.js +294 -0
- package/dist/core/init-wizard.d.ts +26 -0
- package/dist/core/init-wizard.js +168 -0
- package/dist/core/interactive-registry.d.ts +50 -0
- package/dist/core/interactive-registry.js +86 -0
- package/dist/core/interactive.d.ts +48 -0
- package/dist/core/interactive.js +137 -0
- package/dist/core/logger.d.ts +16 -0
- package/dist/core/logger.js +46 -0
- package/dist/core/memory.d.ts +28 -0
- package/dist/core/memory.js +70 -0
- package/dist/core/metered.d.ts +9 -0
- package/dist/core/metered.js +16 -0
- package/dist/core/model.d.ts +74 -0
- package/dist/core/model.js +199 -0
- package/dist/core/pipeline.d.ts +33 -0
- package/dist/core/pipeline.js +223 -0
- package/dist/core/plugins.d.ts +21 -0
- package/dist/core/plugins.js +38 -0
- package/dist/core/probe.d.ts +48 -0
- package/dist/core/probe.js +156 -0
- package/dist/core/profiles.d.ts +42 -0
- package/dist/core/profiles.js +153 -0
- package/dist/core/providers.d.ts +84 -0
- package/dist/core/providers.js +275 -0
- package/dist/core/recall.d.ts +29 -0
- package/dist/core/recall.js +83 -0
- package/dist/core/registry.d.ts +41 -0
- package/dist/core/registry.js +162 -0
- package/dist/core/router.d.ts +33 -0
- package/dist/core/router.js +40 -0
- package/dist/core/sandbox.d.ts +78 -0
- package/dist/core/sandbox.js +268 -0
- package/dist/core/session.d.ts +105 -0
- package/dist/core/session.js +252 -0
- package/dist/core/skills.d.ts +56 -0
- package/dist/core/skills.js +289 -0
- package/dist/core/subagent.d.ts +40 -0
- package/dist/core/subagent.js +55 -0
- package/dist/core/supervisor.d.ts +37 -0
- package/dist/core/supervisor.js +40 -0
- package/dist/core/tools.d.ts +39 -0
- package/dist/core/tools.js +159 -0
- package/dist/core/types.d.ts +87 -0
- package/dist/core/types.js +10 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1032 -0
- package/dist/phases/execute.d.ts +39 -0
- package/dist/phases/execute.js +166 -0
- package/dist/phases/plan.d.ts +11 -0
- package/dist/phases/plan.js +118 -0
- package/dist/phases/scout.d.ts +10 -0
- package/dist/phases/scout.js +113 -0
- package/dist/phases/verify.d.ts +22 -0
- package/dist/phases/verify.js +81 -0
- package/dist/server/daemon.d.ts +50 -0
- package/dist/server/daemon.js +377 -0
- package/dist/server/relay-bridge.d.ts +44 -0
- package/dist/server/relay-bridge.js +175 -0
- package/package.json +39 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EXECUTE phase: dispatch the winning plan to the resolved worker.
|
|
3
|
+
*
|
|
4
|
+
* - target = an AI CLI -> invoke it headless with the plan as the task,
|
|
5
|
+
* wrapping its stdout/stderr through Headroom compression before it would
|
|
6
|
+
* reach any downstream model.
|
|
7
|
+
* - target = "none" -> run the plan's steps as raw commands (the non-AI
|
|
8
|
+
* device-control / bash path), still via safe array-arg execution.
|
|
9
|
+
*
|
|
10
|
+
* All execution goes through execSafe (shell:false, argument arrays) so
|
|
11
|
+
* model/user-provided values can never be shell-interpreted.
|
|
12
|
+
*/
|
|
13
|
+
import type { ExecuteResult, Plan, MaqEvent } from "../core/types.js";
|
|
14
|
+
import { type StreamLine } from "../core/exec.js";
|
|
15
|
+
import { Headroom } from "../core/headroom.js";
|
|
16
|
+
export interface ExecuteOptions {
|
|
17
|
+
cwd: string;
|
|
18
|
+
target: string;
|
|
19
|
+
binPath: string | null;
|
|
20
|
+
/** When true, do not actually run anything (plan-only dry run). */
|
|
21
|
+
dryRun?: boolean;
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
headroom?: Headroom;
|
|
24
|
+
/** Real-time normalized event sink (agent.stdout/stderr, tool.call, ...). */
|
|
25
|
+
onEvent?: (e: MaqEvent) => void;
|
|
26
|
+
/** Whether the worker emits a stable JSON-lines stream (parse structurally). */
|
|
27
|
+
jsonStream?: boolean;
|
|
28
|
+
/** Abort signal; kills the worker/raw child process when aborted. */
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Normalize one worker output line into a MaqEvent. JSON lines from stable
|
|
33
|
+
* streams (e.g. Claude Code) become structured `agent.event`/`tool.call`;
|
|
34
|
+
* everything else is passthrough `agent.stdout`/`agent.stderr`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function normalizeLine(l: StreamLine): MaqEvent;
|
|
37
|
+
/** Split a raw command string into argv without invoking a shell. */
|
|
38
|
+
export declare function tokenizeCommand(cmd: string): string[];
|
|
39
|
+
export declare function runExecute(plan: Plan, opts: ExecuteOptions): Promise<ExecuteResult>;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EXECUTE phase: dispatch the winning plan to the resolved worker.
|
|
3
|
+
*
|
|
4
|
+
* - target = an AI CLI -> invoke it headless with the plan as the task,
|
|
5
|
+
* wrapping its stdout/stderr through Headroom compression before it would
|
|
6
|
+
* reach any downstream model.
|
|
7
|
+
* - target = "none" -> run the plan's steps as raw commands (the non-AI
|
|
8
|
+
* device-control / bash path), still via safe array-arg execution.
|
|
9
|
+
*
|
|
10
|
+
* All execution goes through execSafe (shell:false, argument arrays) so
|
|
11
|
+
* model/user-provided values can never be shell-interpreted.
|
|
12
|
+
*/
|
|
13
|
+
import { makeEvent } from "../core/types.js";
|
|
14
|
+
import { execStream } from "../core/exec.js";
|
|
15
|
+
import { agentSpec } from "../core/registry.js";
|
|
16
|
+
import { Headroom } from "../core/headroom.js";
|
|
17
|
+
/**
|
|
18
|
+
* Normalize one worker output line into a MaqEvent. JSON lines from stable
|
|
19
|
+
* streams (e.g. Claude Code) become structured `agent.event`/`tool.call`;
|
|
20
|
+
* everything else is passthrough `agent.stdout`/`agent.stderr`.
|
|
21
|
+
*/
|
|
22
|
+
export function normalizeLine(l) {
|
|
23
|
+
if (l.stream === "stderr") {
|
|
24
|
+
return makeEvent("agent.stderr", { text: l.line });
|
|
25
|
+
}
|
|
26
|
+
const trimmed = l.line.trim();
|
|
27
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
28
|
+
try {
|
|
29
|
+
const obj = JSON.parse(trimmed);
|
|
30
|
+
const type = String(obj.type ?? obj.event ?? "");
|
|
31
|
+
if (/tool/i.test(type) || obj.tool_use || obj.tool_name || obj.name) {
|
|
32
|
+
return makeEvent("tool.call", { raw: obj, tool: obj.tool_name ?? obj.name ?? obj.tool });
|
|
33
|
+
}
|
|
34
|
+
return makeEvent("agent.event", { raw: obj });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* not JSON after all */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return makeEvent("agent.stdout", { text: l.line });
|
|
41
|
+
}
|
|
42
|
+
/** Split a raw command string into argv without invoking a shell. */
|
|
43
|
+
export function tokenizeCommand(cmd) {
|
|
44
|
+
const out = [];
|
|
45
|
+
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
46
|
+
let m;
|
|
47
|
+
while ((m = re.exec(cmd)) !== null) {
|
|
48
|
+
out.push(m[1] ?? m[2] ?? m[3] ?? "");
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
export async function runExecute(plan, opts) {
|
|
53
|
+
const headroom = opts.headroom ?? new Headroom();
|
|
54
|
+
if (opts.target === "none") {
|
|
55
|
+
return runRaw(plan, opts, headroom);
|
|
56
|
+
}
|
|
57
|
+
return runAgent(plan, opts, headroom);
|
|
58
|
+
}
|
|
59
|
+
async function runAgent(plan, opts, headroom) {
|
|
60
|
+
const spec = agentSpec(opts.target);
|
|
61
|
+
if (!spec || !spec.headless || !opts.binPath) {
|
|
62
|
+
return {
|
|
63
|
+
status: "skipped",
|
|
64
|
+
target: opts.target,
|
|
65
|
+
command: null,
|
|
66
|
+
exitCode: null,
|
|
67
|
+
stdout: "",
|
|
68
|
+
stderr: `agent '${opts.target}' not runnable (no binary or headless template)`,
|
|
69
|
+
filesChanged: [],
|
|
70
|
+
errors: [`agent '${opts.target}' not runnable`],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const taskText = [plan.winner.summary, ...plan.winner.steps.map((s) => `- ${s}`)].join("\n");
|
|
74
|
+
const args = spec.headless.map((a) => (a === "{task}" ? taskText : a));
|
|
75
|
+
const command = [opts.binPath, ...args];
|
|
76
|
+
if (opts.dryRun) {
|
|
77
|
+
return dry(opts.target, command);
|
|
78
|
+
}
|
|
79
|
+
const outcome = await execStream(opts.binPath, args, {
|
|
80
|
+
cwd: opts.cwd,
|
|
81
|
+
timeoutMs: opts.timeoutMs ?? 120000,
|
|
82
|
+
signal: opts.signal,
|
|
83
|
+
onLine: (l) => opts.onEvent?.(normalizeLine(l)),
|
|
84
|
+
});
|
|
85
|
+
// Compress worker output through Headroom (this is where token savings land
|
|
86
|
+
// when the output is fed back to a model for verification/summarization).
|
|
87
|
+
const compressed = headroom.compress(outcome.stdout);
|
|
88
|
+
return {
|
|
89
|
+
status: outcome.code === 0 ? "success" : "failed",
|
|
90
|
+
target: opts.target,
|
|
91
|
+
command,
|
|
92
|
+
exitCode: outcome.code,
|
|
93
|
+
stdout: compressed.compressed,
|
|
94
|
+
stderr: outcome.stderr,
|
|
95
|
+
filesChanged: [],
|
|
96
|
+
errors: outcome.code === 0 ? [] : [outcome.stderr.slice(0, 500)],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function runRaw(plan, opts, headroom) {
|
|
100
|
+
const commands = plan.winner.steps
|
|
101
|
+
.map((s) => extractShellCommand(s))
|
|
102
|
+
.filter((c) => c !== null);
|
|
103
|
+
if (commands.length === 0) {
|
|
104
|
+
return {
|
|
105
|
+
status: "skipped",
|
|
106
|
+
target: "none",
|
|
107
|
+
command: null,
|
|
108
|
+
exitCode: null,
|
|
109
|
+
stdout: "",
|
|
110
|
+
stderr: "no runnable commands found in plan steps (target=none)",
|
|
111
|
+
filesChanged: [],
|
|
112
|
+
errors: [],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (opts.dryRun) {
|
|
116
|
+
return dry("none", commands.flatMap(tokenizeCommand));
|
|
117
|
+
}
|
|
118
|
+
let stdout = "";
|
|
119
|
+
let stderr = "";
|
|
120
|
+
let lastCode = 0;
|
|
121
|
+
for (const cmd of commands) {
|
|
122
|
+
const argv = tokenizeCommand(cmd);
|
|
123
|
+
if (argv.length === 0)
|
|
124
|
+
continue;
|
|
125
|
+
opts.onEvent?.(makeEvent("tool.call", { command: argv }));
|
|
126
|
+
const outcome = await execStream(argv[0], argv.slice(1), {
|
|
127
|
+
cwd: opts.cwd,
|
|
128
|
+
timeoutMs: opts.timeoutMs ?? 60000,
|
|
129
|
+
signal: opts.signal,
|
|
130
|
+
onLine: (l) => opts.onEvent?.(normalizeLine(l)),
|
|
131
|
+
});
|
|
132
|
+
stdout += headroom.compress(outcome.stdout).compressed + "\n";
|
|
133
|
+
stderr += outcome.stderr;
|
|
134
|
+
lastCode = outcome.code;
|
|
135
|
+
if (outcome.code !== 0)
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
status: lastCode === 0 ? "success" : "failed",
|
|
140
|
+
target: "none",
|
|
141
|
+
command: commands.flatMap(tokenizeCommand),
|
|
142
|
+
exitCode: lastCode,
|
|
143
|
+
stdout: stdout.trim(),
|
|
144
|
+
stderr: stderr.trim(),
|
|
145
|
+
filesChanged: [],
|
|
146
|
+
errors: lastCode === 0 ? [] : [stderr.slice(0, 500)],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** Steps prefixed with "$ " or "run: " are treated as literal shell commands. */
|
|
150
|
+
function extractShellCommand(step) {
|
|
151
|
+
const t = step.trim();
|
|
152
|
+
const m = t.match(/^(?:\$\s+|run:\s*)(.+)$/i);
|
|
153
|
+
return m ? m[1].trim() : null;
|
|
154
|
+
}
|
|
155
|
+
function dry(target, command) {
|
|
156
|
+
return {
|
|
157
|
+
status: "skipped",
|
|
158
|
+
target,
|
|
159
|
+
command,
|
|
160
|
+
exitCode: null,
|
|
161
|
+
stdout: "[dry-run] would execute the above command",
|
|
162
|
+
stderr: "",
|
|
163
|
+
filesChanged: [],
|
|
164
|
+
errors: [],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLAN phase: branch -> filter -> commit (verifier-gated Best-of-N).
|
|
3
|
+
*
|
|
4
|
+
* Generates a small set of short candidate approaches via the master model,
|
|
5
|
+
* scores each with a cheap verifier, and early-exits as soon as one clearly
|
|
6
|
+
* passes. This is the efficient variant of test-time search: we do NOT execute
|
|
7
|
+
* every branch; only the winner gets the full execution budget.
|
|
8
|
+
*/
|
|
9
|
+
import type { Plan, ScoutFindings } from "../core/types.js";
|
|
10
|
+
import type { ModelProvider } from "../core/model.js";
|
|
11
|
+
export declare function runPlan(findings: ScoutFindings, provider: ModelProvider, model: string, skills?: string): Promise<Plan>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLAN phase: branch -> filter -> commit (verifier-gated Best-of-N).
|
|
3
|
+
*
|
|
4
|
+
* Generates a small set of short candidate approaches via the master model,
|
|
5
|
+
* scores each with a cheap verifier, and early-exits as soon as one clearly
|
|
6
|
+
* passes. This is the efficient variant of test-time search: we do NOT execute
|
|
7
|
+
* every branch; only the winner gets the full execution budget.
|
|
8
|
+
*/
|
|
9
|
+
const PLAN_SYSTEM = "MODE:plan You are a planning assistant. Return 2-3 short candidate approaches " +
|
|
10
|
+
"as a JSON array of objects with 'summary' and 'steps' fields. Be concise.";
|
|
11
|
+
export async function runPlan(findings, provider, model, skills) {
|
|
12
|
+
const userPrompt = buildPlanPrompt(findings, skills);
|
|
13
|
+
const res = await provider.complete({
|
|
14
|
+
model,
|
|
15
|
+
tier: findings.complexity === "complex" ? "strong" : "cheap",
|
|
16
|
+
maxTokens: 512,
|
|
17
|
+
messages: [
|
|
18
|
+
{ role: "system", content: PLAN_SYSTEM },
|
|
19
|
+
{ role: "user", content: userPrompt },
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
const raw = parseCandidates(res.text);
|
|
23
|
+
const evaluated = [];
|
|
24
|
+
let earlyExit = false;
|
|
25
|
+
for (const c of raw) {
|
|
26
|
+
const scored = scoreCandidate(c, findings);
|
|
27
|
+
evaluated.push(scored);
|
|
28
|
+
// Early exit: first clearly-passing candidate wins; stop spending tokens.
|
|
29
|
+
if (scored.pass && scored.score >= 0.7) {
|
|
30
|
+
earlyExit = true;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const winner = pickWinner(evaluated);
|
|
35
|
+
return {
|
|
36
|
+
winner,
|
|
37
|
+
allEvaluated: evaluated,
|
|
38
|
+
candidatesGenerated: raw.length,
|
|
39
|
+
earlyExit,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function buildPlanPrompt(f, skills) {
|
|
43
|
+
return [
|
|
44
|
+
skills ? skills + "\n" : "",
|
|
45
|
+
`Task: ${f.task}`,
|
|
46
|
+
`Working dir: ${f.cwd}`,
|
|
47
|
+
`Complexity: ${f.complexity}`,
|
|
48
|
+
`Relevant files: ${f.files.slice(0, 20).join(", ") || "(none identified)"}`,
|
|
49
|
+
f.manifestSnippet ? `Manifest present: yes` : `Manifest present: no`,
|
|
50
|
+
`Constraints: keep the change set minimal; do not break existing tests.`,
|
|
51
|
+
]
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.join("\n");
|
|
54
|
+
}
|
|
55
|
+
function parseCandidates(text) {
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(text);
|
|
58
|
+
if (Array.isArray(parsed)) {
|
|
59
|
+
return parsed
|
|
60
|
+
.filter((c) => c && typeof c.summary === "string")
|
|
61
|
+
.map((c) => ({
|
|
62
|
+
summary: String(c.summary),
|
|
63
|
+
steps: Array.isArray(c.steps) ? c.steps.map(String) : [],
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* fall through */
|
|
69
|
+
}
|
|
70
|
+
// Fallback: single candidate from raw text.
|
|
71
|
+
return [{ summary: text.slice(0, 200).trim() || "proceed with the task", steps: [] }];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Cheap, grounded verifier. Rewards candidates that reference scoped files and
|
|
75
|
+
* concrete, testable steps; penalizes vague or empty plans. Grounding the
|
|
76
|
+
* score in checkable structure (not vibes) avoids the self-consistency bias trap.
|
|
77
|
+
*/
|
|
78
|
+
function scoreCandidate(c, f) {
|
|
79
|
+
let score = 0.5;
|
|
80
|
+
const reasons = [];
|
|
81
|
+
if (c.steps.length >= 2) {
|
|
82
|
+
score += 0.2;
|
|
83
|
+
reasons.push("has concrete steps");
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
score -= 0.2;
|
|
87
|
+
reasons.push("too few steps");
|
|
88
|
+
}
|
|
89
|
+
const mentionsTest = /test|lint|verify|typecheck/i.test(c.steps.join(" ") + c.summary);
|
|
90
|
+
if (mentionsTest) {
|
|
91
|
+
score += 0.2;
|
|
92
|
+
reasons.push("includes a verification step");
|
|
93
|
+
}
|
|
94
|
+
const mentionsScopedFile = f.files.some((file) => (c.steps.join(" ") + c.summary).toLowerCase().includes(baseName(file).toLowerCase()));
|
|
95
|
+
if (mentionsScopedFile) {
|
|
96
|
+
score += 0.1;
|
|
97
|
+
reasons.push("references scoped file");
|
|
98
|
+
}
|
|
99
|
+
score = Math.max(0, Math.min(1, score));
|
|
100
|
+
return {
|
|
101
|
+
summary: c.summary,
|
|
102
|
+
steps: c.steps,
|
|
103
|
+
score,
|
|
104
|
+
pass: score >= 0.6,
|
|
105
|
+
reason: reasons.join("; "),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function pickWinner(cands) {
|
|
109
|
+
if (cands.length === 0) {
|
|
110
|
+
return { summary: "proceed with the task", steps: [], score: 0.5, pass: true, reason: "no candidates; default" };
|
|
111
|
+
}
|
|
112
|
+
return [...cands].sort((a, b) => b.score - a.score)[0];
|
|
113
|
+
}
|
|
114
|
+
function baseName(p) {
|
|
115
|
+
const parts = p.split(/[\\/]/);
|
|
116
|
+
const name = parts[parts.length - 1] ?? p;
|
|
117
|
+
return name.replace(/\.[^.]+$/, "");
|
|
118
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOUT phase: deterministic, read-only recon. Zero token cost by default.
|
|
3
|
+
*
|
|
4
|
+
* Explores the working directory (capped), reads README + manifest, gathers
|
|
5
|
+
* recent git history and uncommitted change stats, classifies complexity, and
|
|
6
|
+
* writes a structured findings object. Model-assisted deep research is an
|
|
7
|
+
* explicit opt-in layered on top; the base pass never calls a model.
|
|
8
|
+
*/
|
|
9
|
+
import type { ScoutFindings } from "../core/types.js";
|
|
10
|
+
export declare function runScout(task: string, cwd: string, fileCap?: number): Promise<ScoutFindings>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCOUT phase: deterministic, read-only recon. Zero token cost by default.
|
|
3
|
+
*
|
|
4
|
+
* Explores the working directory (capped), reads README + manifest, gathers
|
|
5
|
+
* recent git history and uncommitted change stats, classifies complexity, and
|
|
6
|
+
* writes a structured findings object. Model-assisted deep research is an
|
|
7
|
+
* explicit opt-in layered on top; the base pass never calls a model.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
10
|
+
import { join, relative } from "node:path";
|
|
11
|
+
import { classifyComplexity } from "../core/complexity.js";
|
|
12
|
+
import { execSafe } from "../core/exec.js";
|
|
13
|
+
const IGNORE = new Set([
|
|
14
|
+
"node_modules",
|
|
15
|
+
".git",
|
|
16
|
+
"dist",
|
|
17
|
+
"build",
|
|
18
|
+
"out",
|
|
19
|
+
".cache",
|
|
20
|
+
"target",
|
|
21
|
+
".next",
|
|
22
|
+
"coverage",
|
|
23
|
+
]);
|
|
24
|
+
const MANIFESTS = [
|
|
25
|
+
"package.json",
|
|
26
|
+
"Cargo.toml",
|
|
27
|
+
"pyproject.toml",
|
|
28
|
+
"go.mod",
|
|
29
|
+
"pom.xml",
|
|
30
|
+
"build.gradle",
|
|
31
|
+
"pubspec.yaml",
|
|
32
|
+
];
|
|
33
|
+
function walk(root, cap) {
|
|
34
|
+
const found = [];
|
|
35
|
+
const stack = [root];
|
|
36
|
+
while (stack.length && found.length < cap) {
|
|
37
|
+
const dir = stack.pop();
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = readdirSync(dir);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (IGNORE.has(entry))
|
|
47
|
+
continue;
|
|
48
|
+
const full = join(dir, entry);
|
|
49
|
+
let st;
|
|
50
|
+
try {
|
|
51
|
+
st = statSync(full);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (st.isDirectory()) {
|
|
57
|
+
stack.push(full);
|
|
58
|
+
}
|
|
59
|
+
else if (st.isFile()) {
|
|
60
|
+
found.push(relative(root, full));
|
|
61
|
+
if (found.length >= cap)
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return found.sort();
|
|
67
|
+
}
|
|
68
|
+
function readIfExists(path, maxLen) {
|
|
69
|
+
if (!existsSync(path))
|
|
70
|
+
return null;
|
|
71
|
+
try {
|
|
72
|
+
return readFileSync(path, "utf8").slice(0, maxLen);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function runScout(task, cwd, fileCap = 400) {
|
|
79
|
+
const files = walk(cwd, fileCap);
|
|
80
|
+
const readmePath = ["README.md", "readme.md", "README"].map((f) => join(cwd, f)).find(existsSync);
|
|
81
|
+
const readme = readmePath ? readIfExists(readmePath, 2000) : null;
|
|
82
|
+
const manifestName = MANIFESTS.find((m) => existsSync(join(cwd, m)));
|
|
83
|
+
const manifestSnippet = manifestName ? readIfExists(join(cwd, manifestName), 1500) : null;
|
|
84
|
+
// Git context is best-effort; missing git is fine.
|
|
85
|
+
const log = await execSafe("git", ["log", "--oneline", "-n", "10"], { cwd, timeoutMs: 5000 });
|
|
86
|
+
const recentCommits = log.code === 0 ? log.stdout.split(/\r?\n/).filter(Boolean) : [];
|
|
87
|
+
const diff = await execSafe("git", ["diff", "--stat"], { cwd, timeoutMs: 5000 });
|
|
88
|
+
const uncommittedChanges = diff.code === 0 ? diff.stdout.trim() : "";
|
|
89
|
+
const relevant = files.filter((f) => task.toLowerCase().includes(baseName(f).toLowerCase()));
|
|
90
|
+
const { complexity, reasons } = classifyComplexity(task, relevant);
|
|
91
|
+
const notes = [];
|
|
92
|
+
if (!manifestName)
|
|
93
|
+
notes.push("no recognized build manifest found");
|
|
94
|
+
if (recentCommits.length === 0)
|
|
95
|
+
notes.push("no git history (or git unavailable)");
|
|
96
|
+
notes.push(`complexity=${complexity}: ${reasons.join("; ") || "no strong signals"}`);
|
|
97
|
+
return {
|
|
98
|
+
task,
|
|
99
|
+
cwd,
|
|
100
|
+
files: relevant.length ? relevant : files.slice(0, 50),
|
|
101
|
+
readme,
|
|
102
|
+
manifestSnippet,
|
|
103
|
+
recentCommits,
|
|
104
|
+
uncommittedChanges,
|
|
105
|
+
complexity,
|
|
106
|
+
notes,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function baseName(p) {
|
|
110
|
+
const parts = p.split(/[\\/]/);
|
|
111
|
+
const name = parts[parts.length - 1] ?? p;
|
|
112
|
+
return name.replace(/\.[^.]+$/, "");
|
|
113
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VERIFY phase: never trust "the agent stopped talking = done".
|
|
3
|
+
*
|
|
4
|
+
* Order of preference:
|
|
5
|
+
* 1. Run the project's own test command (tests as ground truth).
|
|
6
|
+
* 2. If no test command exists, fall back to a cheap cross-model review of
|
|
7
|
+
* the execution output (the master model judging pass/fail).
|
|
8
|
+
* A hard execution failure (non-zero exit) short-circuits to "not verified".
|
|
9
|
+
*/
|
|
10
|
+
import type { ExecuteResult, VerifyResult } from "../core/types.js";
|
|
11
|
+
import type { ModelProvider } from "../core/model.js";
|
|
12
|
+
export interface VerifyOptions {
|
|
13
|
+
cwd: string;
|
|
14
|
+
provider: ModelProvider;
|
|
15
|
+
model: string;
|
|
16
|
+
/** Skip running tests (e.g. for dry runs). */
|
|
17
|
+
skipTests?: boolean;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
/** Detect a test command from the project manifest. Returns argv or null. */
|
|
21
|
+
export declare function detectTestCommand(cwd: string): string[] | null;
|
|
22
|
+
export declare function runVerify(exec: ExecuteResult, opts: VerifyOptions): Promise<VerifyResult>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VERIFY phase: never trust "the agent stopped talking = done".
|
|
3
|
+
*
|
|
4
|
+
* Order of preference:
|
|
5
|
+
* 1. Run the project's own test command (tests as ground truth).
|
|
6
|
+
* 2. If no test command exists, fall back to a cheap cross-model review of
|
|
7
|
+
* the execution output (the master model judging pass/fail).
|
|
8
|
+
* A hard execution failure (non-zero exit) short-circuits to "not verified".
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { execSafe } from "../core/exec.js";
|
|
13
|
+
const VERIFY_SYSTEM = "MODE:verify You are a strict verifier. Given execution output, return JSON " +
|
|
14
|
+
"{\"pass\": boolean, \"reason\": string}. Fail on any error or incomplete signal.";
|
|
15
|
+
/** Detect a test command from the project manifest. Returns argv or null. */
|
|
16
|
+
export function detectTestCommand(cwd) {
|
|
17
|
+
const pkgPath = join(cwd, "package.json");
|
|
18
|
+
if (existsSync(pkgPath)) {
|
|
19
|
+
try {
|
|
20
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
21
|
+
if (pkg.scripts && typeof pkg.scripts.test === "string") {
|
|
22
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
23
|
+
return [npm, "test", "--silent"];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
/* ignore */
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (existsSync(join(cwd, "Cargo.toml")))
|
|
31
|
+
return ["cargo", "test"];
|
|
32
|
+
if (existsSync(join(cwd, "pyproject.toml")) || existsSync(join(cwd, "pytest.ini"))) {
|
|
33
|
+
return ["pytest", "-q"];
|
|
34
|
+
}
|
|
35
|
+
if (existsSync(join(cwd, "go.mod")))
|
|
36
|
+
return ["go", "test", "./..."];
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
export async function runVerify(exec, opts) {
|
|
40
|
+
if (exec.status === "failed") {
|
|
41
|
+
return { verified: false, method: "execution-status", details: "execution failed (non-zero exit)" };
|
|
42
|
+
}
|
|
43
|
+
if (exec.status === "skipped") {
|
|
44
|
+
return { verified: true, method: "skipped", details: "execution was skipped (dry run or no-op)" };
|
|
45
|
+
}
|
|
46
|
+
const testCmd = opts.skipTests ? null : detectTestCommand(opts.cwd);
|
|
47
|
+
if (testCmd) {
|
|
48
|
+
const outcome = await execSafe(testCmd[0], testCmd.slice(1), {
|
|
49
|
+
cwd: opts.cwd,
|
|
50
|
+
timeoutMs: opts.timeoutMs ?? 300000,
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
verified: outcome.code === 0,
|
|
54
|
+
method: `tests:${testCmd.join(" ")}`,
|
|
55
|
+
details: outcome.code === 0
|
|
56
|
+
? "test suite passed"
|
|
57
|
+
: `test suite failed (exit ${outcome.code}): ${outcome.stderr.slice(0, 300)}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// No tests -> cross-model review of the output as a fallback signal.
|
|
61
|
+
const res = await opts.provider.complete({
|
|
62
|
+
model: opts.model,
|
|
63
|
+
tier: "cheap",
|
|
64
|
+
maxTokens: 128,
|
|
65
|
+
messages: [
|
|
66
|
+
{ role: "system", content: VERIFY_SYSTEM },
|
|
67
|
+
{ role: "user", content: (exec.stdout + "\n" + exec.stderr).slice(0, 4000) },
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
try {
|
|
71
|
+
const verdict = JSON.parse(res.text);
|
|
72
|
+
return {
|
|
73
|
+
verified: Boolean(verdict.pass),
|
|
74
|
+
method: "cross-model-review",
|
|
75
|
+
details: verdict.reason ?? "",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return { verified: true, method: "cross-model-review", details: "no failure signals parsed" };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAQ daemon — a small, dependency-free HTTP + SSE server (Node built-in
|
|
3
|
+
* `http`) that exposes the orchestrator to the app track over one normalized
|
|
4
|
+
* seam. This is the concrete "master <-> app" contract from the product docs:
|
|
5
|
+
* the app never speaks any worker CLI's dialect, only MAQ's normalized events.
|
|
6
|
+
*
|
|
7
|
+
* SECURITY:
|
|
8
|
+
* - Binds to 127.0.0.1 by default (loopback only). Binding to 0.0.0.0 is
|
|
9
|
+
* allowed but logged loudly, because that exposes device control to the LAN.
|
|
10
|
+
* - Every route except /health requires `Authorization: Bearer <token>`.
|
|
11
|
+
* The token comes from MAQ_TOKEN, else config, else a freshly generated one
|
|
12
|
+
* that is printed once on startup. There is no unauthenticated path to run
|
|
13
|
+
* commands. Auth comparison is constant-time.
|
|
14
|
+
* - CORS is opt-in (MAQ_CORS_ORIGIN); off by default.
|
|
15
|
+
*
|
|
16
|
+
* Endpoints (all JSON unless noted):
|
|
17
|
+
* GET /health -> liveness (no auth)
|
|
18
|
+
* GET /v1/agents -> detected worker CLIs
|
|
19
|
+
* GET /v1/connectivity -> connectivity tier probe
|
|
20
|
+
* GET /v1/sessions -> list session summaries
|
|
21
|
+
* POST /v1/sessions -> start a session {task,target?,cwd?,dryRun?}
|
|
22
|
+
* GET /v1/sessions/:id -> one session (summary + events)
|
|
23
|
+
* GET /v1/sessions/:id/events -> SSE stream (replay history, then live)
|
|
24
|
+
* POST /v1/sessions/:id/message -> deliver a message to a session {text}
|
|
25
|
+
*/
|
|
26
|
+
import { type Server } from "node:http";
|
|
27
|
+
import { SessionRegistry } from "../core/session.js";
|
|
28
|
+
export interface DaemonOptions {
|
|
29
|
+
host?: string;
|
|
30
|
+
port?: number;
|
|
31
|
+
token?: string;
|
|
32
|
+
version?: string;
|
|
33
|
+
corsOrigin?: string;
|
|
34
|
+
registry?: SessionRegistry;
|
|
35
|
+
}
|
|
36
|
+
export interface Daemon {
|
|
37
|
+
server: Server;
|
|
38
|
+
registry: SessionRegistry;
|
|
39
|
+
token: string;
|
|
40
|
+
host: string;
|
|
41
|
+
port: number;
|
|
42
|
+
listen(): Promise<{
|
|
43
|
+
host: string;
|
|
44
|
+
port: number;
|
|
45
|
+
}>;
|
|
46
|
+
close(): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/** Generate a URL-safe token. */
|
|
49
|
+
export declare function generateToken(): string;
|
|
50
|
+
export declare function createDaemon(opts?: DaemonOptions): Daemon;
|