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,252 @@
1
+ /**
2
+ * Session registry + multi-agent management.
3
+ *
4
+ * Each session is one orchestrated task run (Scout->Plan->Execute->Verify)
5
+ * against one worker target. The registry runs many concurrently, keeps each
6
+ * session's normalized event history for replay, and exposes a live subscribe
7
+ * hook that the daemon turns into an SSE stream for the app.
8
+ *
9
+ * Coordination vocabulary borrowed from CAO (the vocabulary, not the tmux +
10
+ * Bedrock runtime):
11
+ * - assign(task) -> async, fire-and-forget; returns the session id now
12
+ * - handoff(task) -> sync, wait-for-completion; resolves with the result
13
+ * - sendMessage(id, m) -> deliver a message into a session's inbox
14
+ * Worker context stays isolated per session; the supervisor sees condensed
15
+ * events, never a raw shared transcript.
16
+ */
17
+ import { EventEmitter } from "node:events";
18
+ import { randomUUID } from "node:crypto";
19
+ import { makeEvent } from "./types.js";
20
+ import { runPipeline } from "./pipeline.js";
21
+ import { ProfileManager } from "./profiles.js";
22
+ /** Thrown through the pipeline checkpoint when a session is cancelled. */
23
+ export class CancelError extends Error {
24
+ constructor() {
25
+ super("cancelled");
26
+ this.name = "CancelError";
27
+ }
28
+ }
29
+ export class SessionRegistry {
30
+ sessions = new Map();
31
+ controls = new Map();
32
+ bus = new EventEmitter();
33
+ maxSessions;
34
+ maxEventsPerSession;
35
+ constructor(maxSessions = 100, maxEventsPerSession = 2000) {
36
+ this.maxSessions = maxSessions;
37
+ this.maxEventsPerSession = maxEventsPerSession;
38
+ this.bus.setMaxListeners(0);
39
+ }
40
+ /** Fire-and-forget: start a session and return its id immediately. */
41
+ assign(task, opts = {}) {
42
+ const session = this.spawn(task, opts);
43
+ return session;
44
+ }
45
+ /** Synchronous handoff: start a session and await its completion. */
46
+ async handoff(task, opts = {}) {
47
+ const session = this.spawn(task, opts);
48
+ await this.waitFor(session.id);
49
+ return this.sessions.get(session.id);
50
+ }
51
+ /** Deliver a message into a session's inbox (records a normalized event). */
52
+ sendMessage(id, text, from = "user") {
53
+ const s = this.sessions.get(id);
54
+ if (!s)
55
+ return false;
56
+ const msg = { ts: new Date().toISOString(), from, text };
57
+ s.inbox.push(msg);
58
+ this.push(s, makeEvent("agent.event", { note: "message received", from, text }));
59
+ return true;
60
+ }
61
+ get(id) {
62
+ return this.sessions.get(id);
63
+ }
64
+ list() {
65
+ return [...this.sessions.values()].map((s) => this.summarize(s));
66
+ }
67
+ summarize(s) {
68
+ return {
69
+ id: s.id,
70
+ task: s.task,
71
+ status: s.status,
72
+ target: s.target,
73
+ cwd: s.cwd,
74
+ createdAt: s.createdAt,
75
+ updatedAt: s.updatedAt,
76
+ verified: s.result?.verify.verified,
77
+ eventCount: s.events.length,
78
+ };
79
+ }
80
+ /** Subscribe to live events for a session. Returns an unsubscribe fn. */
81
+ subscribe(id, listener) {
82
+ const evt = `event:${id}`;
83
+ this.bus.on(evt, listener);
84
+ return () => this.bus.off(evt, listener);
85
+ }
86
+ /** Subscribe to ALL sessions' events (for plugins/webhooks). */
87
+ onAny(listener) {
88
+ this.bus.on("event", listener);
89
+ }
90
+ offAny(listener) {
91
+ this.bus.off("event", listener);
92
+ }
93
+ /** Remove finished sessions (housekeeping). */
94
+ prune() {
95
+ let removed = 0;
96
+ for (const [id, s] of this.sessions) {
97
+ if (s.status !== "running") {
98
+ this.sessions.delete(id);
99
+ removed++;
100
+ }
101
+ }
102
+ return removed;
103
+ }
104
+ spawn(task, opts) {
105
+ if (this.sessions.size >= this.maxSessions)
106
+ this.evictOldestDone();
107
+ const id = randomUUID();
108
+ const now = new Date().toISOString();
109
+ // Resolve an agent profile (if any) into concrete overrides.
110
+ let target = opts.target;
111
+ let provider = opts.provider;
112
+ let model = opts.model;
113
+ if (opts.profile) {
114
+ const prof = new ProfileManager(opts.cwd ?? process.cwd()).get(opts.profile);
115
+ if (prof) {
116
+ target = target ?? prof.target;
117
+ provider = provider ?? prof.provider;
118
+ model = model ?? prof.model;
119
+ }
120
+ }
121
+ const session = {
122
+ id,
123
+ task,
124
+ status: "running",
125
+ target,
126
+ cwd: opts.cwd ?? process.cwd(),
127
+ createdAt: now,
128
+ updatedAt: now,
129
+ events: [],
130
+ inbox: [],
131
+ };
132
+ this.sessions.set(id, session);
133
+ const control = { paused: false, cancelled: false, abort: new AbortController(), resumeWaiters: [] };
134
+ this.controls.set(id, control);
135
+ const checkpoint = () => {
136
+ const c = this.controls.get(id);
137
+ if (!c || c.cancelled)
138
+ return Promise.reject(new CancelError());
139
+ if (!c.paused)
140
+ return Promise.resolve();
141
+ return new Promise((resolve, reject) => {
142
+ c.resumeWaiters.push(() => (this.controls.get(id)?.cancelled ? reject(new CancelError()) : resolve()));
143
+ });
144
+ };
145
+ const pipelineOpts = {
146
+ cwd: session.cwd,
147
+ target,
148
+ provider,
149
+ model,
150
+ dryRun: opts.dryRun,
151
+ timeoutMs: opts.timeoutMs,
152
+ onEvent: (e) => this.push(session, e),
153
+ signal: control.abort.signal,
154
+ checkpoint,
155
+ };
156
+ // Run asynchronously; never throw out of assign/handoff spawn.
157
+ runPipeline(task, pipelineOpts)
158
+ .then((result) => {
159
+ if (session.status === "cancelled")
160
+ return;
161
+ session.result = result;
162
+ session.status = "done";
163
+ session.updatedAt = new Date().toISOString();
164
+ this.bus.emit(`done:${id}`);
165
+ })
166
+ .catch((err) => {
167
+ if (err instanceof CancelError || this.controls.get(id)?.cancelled) {
168
+ session.status = "cancelled";
169
+ session.updatedAt = new Date().toISOString();
170
+ this.push(session, makeEvent("task.cancelled", {}));
171
+ }
172
+ else {
173
+ session.error = err instanceof Error ? err.message : String(err);
174
+ session.status = "error";
175
+ session.updatedAt = new Date().toISOString();
176
+ this.push(session, makeEvent("task.error", { message: session.error }));
177
+ }
178
+ this.bus.emit(`done:${id}`);
179
+ });
180
+ return session;
181
+ }
182
+ /** Pause a running session at its next phase boundary. */
183
+ pause(id) {
184
+ const c = this.controls.get(id);
185
+ const s = this.sessions.get(id);
186
+ if (!c || !s || c.cancelled || s.status !== "running")
187
+ return false;
188
+ c.paused = true;
189
+ s.status = "paused";
190
+ this.push(s, makeEvent("task.paused", {}));
191
+ return true;
192
+ }
193
+ /** Resume a paused session. */
194
+ resume(id) {
195
+ const c = this.controls.get(id);
196
+ const s = this.sessions.get(id);
197
+ if (!c || !s || !c.paused)
198
+ return false;
199
+ c.paused = false;
200
+ s.status = "running";
201
+ const waiters = c.resumeWaiters.splice(0);
202
+ for (const w of waiters)
203
+ w();
204
+ this.push(s, makeEvent("task.resumed", {}));
205
+ return true;
206
+ }
207
+ /** Cancel a session; kills in-flight worker processes and aborts the run. */
208
+ cancel(id) {
209
+ const c = this.controls.get(id);
210
+ const s = this.sessions.get(id);
211
+ if (!c || !s || s.status === "done" || s.status === "error" || s.status === "cancelled")
212
+ return false;
213
+ c.cancelled = true;
214
+ c.paused = false;
215
+ try {
216
+ c.abort.abort();
217
+ }
218
+ catch {
219
+ /* ignore */
220
+ }
221
+ const waiters = c.resumeWaiters.splice(0);
222
+ for (const w of waiters)
223
+ w();
224
+ return true;
225
+ }
226
+ push(session, e) {
227
+ session.events.push(e);
228
+ // Bound per-session history so a long-lived or chatty session cannot grow
229
+ // memory without limit. SSE replay still gets recent context; the terminal
230
+ // task.started/… bookkeeping stays intact because we only drop the oldest.
231
+ if (session.events.length > this.maxEventsPerSession) {
232
+ session.events.splice(0, session.events.length - this.maxEventsPerSession);
233
+ }
234
+ session.updatedAt = e.ts;
235
+ this.bus.emit(`event:${session.id}`, e);
236
+ this.bus.emit("event", { sessionId: session.id, event: e });
237
+ }
238
+ waitFor(id) {
239
+ const s = this.sessions.get(id);
240
+ if (!s || s.status !== "running")
241
+ return Promise.resolve();
242
+ return new Promise((resolve) => this.bus.once(`done:${id}`, () => resolve()));
243
+ }
244
+ evictOldestDone() {
245
+ for (const [id, s] of this.sessions) {
246
+ if (s.status !== "running") {
247
+ this.sessions.delete(id);
248
+ return;
249
+ }
250
+ }
251
+ }
252
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Skills / rules layer — standing instructions injected into the master's own
3
+ * prompts (Plan, Verify), the way `CLAUDE.md` / `AGENTS.md` / skill files lift a
4
+ * model's behavior.
5
+ *
6
+ * Key nuance (from field reports on strong models): rules written to scaffold a
7
+ * WEAK model can HOLD BACK a strong one — guardrails for failure modes it no
8
+ * longer has just eat context. So skills are TIER-AWARE: a file can declare the
9
+ * tier it applies to, and the strong tier receives a trimmed, high-signal set.
10
+ *
11
+ * Sources of skills (later ones win on name collision):
12
+ * 1. built-in defaults (below)
13
+ * 2. ~/.maqcli/skills/*.md (global)
14
+ * 3. <cwd>/<skillsDir>/*.md (project; default ".maq/skills")
15
+ * 4. <cwd>/AGENTS.md, <cwd>/CLAUDE.md, <cwd>/MAQ.md (project standing files)
16
+ *
17
+ * A skill file may begin with light front-matter lines:
18
+ * maq-tier: cheap | strong | all (default: all)
19
+ * maq-kind: constraint | scaffolding | context (default: context)
20
+ * `scaffolding` skills are dropped for the strong tier.
21
+ */
22
+ import type { Tier } from "./router.js";
23
+ export type SkillTier = "cheap" | "strong" | "all";
24
+ export type SkillKind = "constraint" | "scaffolding" | "context";
25
+ export interface Skill {
26
+ name: string;
27
+ path: string | null;
28
+ tier: SkillTier;
29
+ kind: SkillKind;
30
+ content: string;
31
+ }
32
+ export interface SkillsOptions {
33
+ skillsDir?: string;
34
+ /** Hard cap on the assembled context block (chars). */
35
+ maxChars?: number;
36
+ }
37
+ export declare class SkillsManager {
38
+ private cwd;
39
+ private skillsDir;
40
+ private maxChars;
41
+ constructor(cwd: string, opts?: SkillsOptions);
42
+ /** Load all skills from every source, de-duplicated by name (later wins). */
43
+ load(): Skill[];
44
+ /** Skills that apply to a given tier (strong drops `scaffolding`). */
45
+ forTier(tier: Tier): Skill[];
46
+ /** Assemble a compact standing-instructions block for a tier (capped). */
47
+ contextBlock(tier: Tier): string;
48
+ private readDir;
49
+ private readFile;
50
+ }
51
+ /**
52
+ * Scaffold starter skill files (and an AGENTS.md if absent) into a project so
53
+ * teams can extend the built-in pack. Never overwrites existing files.
54
+ * Returns the list of files written.
55
+ */
56
+ export declare function initSkills(cwd: string, skillsDir?: string): string[];
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Skills / rules layer — standing instructions injected into the master's own
3
+ * prompts (Plan, Verify), the way `CLAUDE.md` / `AGENTS.md` / skill files lift a
4
+ * model's behavior.
5
+ *
6
+ * Key nuance (from field reports on strong models): rules written to scaffold a
7
+ * WEAK model can HOLD BACK a strong one — guardrails for failure modes it no
8
+ * longer has just eat context. So skills are TIER-AWARE: a file can declare the
9
+ * tier it applies to, and the strong tier receives a trimmed, high-signal set.
10
+ *
11
+ * Sources of skills (later ones win on name collision):
12
+ * 1. built-in defaults (below)
13
+ * 2. ~/.maqcli/skills/*.md (global)
14
+ * 3. <cwd>/<skillsDir>/*.md (project; default ".maq/skills")
15
+ * 4. <cwd>/AGENTS.md, <cwd>/CLAUDE.md, <cwd>/MAQ.md (project standing files)
16
+ *
17
+ * A skill file may begin with light front-matter lines:
18
+ * maq-tier: cheap | strong | all (default: all)
19
+ * maq-kind: constraint | scaffolding | context (default: context)
20
+ * `scaffolding` skills are dropped for the strong tier.
21
+ */
22
+ import { homedir } from "node:os";
23
+ import { join } from "node:path";
24
+ import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, writeFileSync } from "node:fs";
25
+ /**
26
+ * Curated default skills. Grounded in 2026 agent-instruction best practice:
27
+ * frontier models reliably follow ~150–200 instructions before adherence
28
+ * degrades, so this set is deliberately small and high-signal (constraints that
29
+ * matter + scaffolding only weak tiers need). `scaffolding` skills are dropped
30
+ * for the strong tier because rules written for a weak model hold a strong one
31
+ * back. Projects extend/override these via .maq/skills, AGENTS.md, CLAUDE.md.
32
+ */
33
+ const DEFAULT_SKILLS = [
34
+ // --- constraints (apply to every tier) ---
35
+ {
36
+ name: "verify-before-done",
37
+ path: null,
38
+ tier: "all",
39
+ kind: "constraint",
40
+ content: "Never treat 'the agent stopped' as done. Prove completion with tests, a build, or a concrete check. State what you verified and what you could not.",
41
+ },
42
+ {
43
+ name: "tests-are-truth",
44
+ path: null,
45
+ tier: "all",
46
+ kind: "constraint",
47
+ content: "Run the project's own tests/build as the completion signal. When fixing a bug, add or adjust a test that would have caught it.",
48
+ },
49
+ {
50
+ name: "minimal-diff",
51
+ path: null,
52
+ tier: "all",
53
+ kind: "constraint",
54
+ content: "Make the smallest change that satisfies the task. Do not refactor or reformat unrelated code. Match the project's existing style, conventions, and libraries.",
55
+ },
56
+ {
57
+ name: "safe-exec",
58
+ path: null,
59
+ tier: "all",
60
+ kind: "constraint",
61
+ content: "Run commands via argument arrays, never string-interpolate untrusted values into a shell. Validate inputs. Quote/escape anything user- or model-provided.",
62
+ },
63
+ {
64
+ name: "confirm-destructive",
65
+ path: null,
66
+ tier: "all",
67
+ kind: "constraint",
68
+ content: "Pause and confirm before irreversible or high-blast-radius actions: force push, hard reset, recursive delete, dropping data, or touching production.",
69
+ },
70
+ {
71
+ name: "no-secrets",
72
+ path: null,
73
+ tier: "all",
74
+ kind: "constraint",
75
+ content: "Never print or commit secrets. Reference credentials by key name, not value. Flag files that likely hold secrets before reading or committing them.",
76
+ },
77
+ {
78
+ name: "cite-what-you-checked",
79
+ path: null,
80
+ tier: "all",
81
+ kind: "context",
82
+ content: "Distinguish what you verified (ran/read/confirmed) from what you assumed. Do not present assumptions as facts; do not over-qualify confirmed results.",
83
+ },
84
+ {
85
+ name: "keep-rules-fresh",
86
+ path: null,
87
+ tier: "all",
88
+ kind: "context",
89
+ content: "Standing rules can go stale. If an instruction contradicts what you observe in the repo or is a guardrail you no longer need, flag it instead of blindly following it.",
90
+ },
91
+ // --- scaffolding (cheap tier only; dropped for strong models) ---
92
+ {
93
+ name: "scope-tight",
94
+ path: null,
95
+ tier: "cheap",
96
+ kind: "scaffolding",
97
+ content: "Work only on the files in scope. Do not re-explore the whole repo. Prefer the smallest change that satisfies the task and keeps existing tests green.",
98
+ },
99
+ {
100
+ name: "read-before-write",
101
+ path: null,
102
+ tier: "cheap",
103
+ kind: "scaffolding",
104
+ content: "Read the relevant file(s) fully before editing them. Understand the surrounding pattern first, then change the minimum.",
105
+ },
106
+ {
107
+ name: "plan-then-act",
108
+ path: null,
109
+ tier: "cheap",
110
+ kind: "scaffolding",
111
+ content: "For non-trivial tasks, outline a short plan (files to touch, steps, how you'll verify) before editing. Commit to one approach.",
112
+ },
113
+ {
114
+ name: "structured-handoff",
115
+ path: null,
116
+ tier: "cheap",
117
+ kind: "scaffolding",
118
+ content: "When delegating a sub-step, pass a minimal context package and expect back a concise summary (findings, decisions, artifacts) — not a raw transcript.",
119
+ },
120
+ ];
121
+ const STANDING_FILES = ["AGENTS.md", "CLAUDE.md", "MAQ.md"];
122
+ const MAX_FILE_CHARS = 8000;
123
+ export class SkillsManager {
124
+ cwd;
125
+ skillsDir;
126
+ maxChars;
127
+ constructor(cwd, opts = {}) {
128
+ this.cwd = cwd;
129
+ this.skillsDir = opts.skillsDir ?? ".maq/skills";
130
+ this.maxChars = opts.maxChars ?? 6000;
131
+ }
132
+ /** Load all skills from every source, de-duplicated by name (later wins). */
133
+ load() {
134
+ const byName = new Map();
135
+ for (const s of DEFAULT_SKILLS)
136
+ byName.set(s.name, s);
137
+ for (const dir of [join(homedir(), ".maqcli", "skills"), join(this.cwd, this.skillsDir)]) {
138
+ for (const s of this.readDir(dir))
139
+ byName.set(s.name, s);
140
+ }
141
+ for (const f of STANDING_FILES) {
142
+ const p = join(this.cwd, f);
143
+ const s = this.readFile(p, f.replace(/\.md$/i, "").toLowerCase());
144
+ if (s)
145
+ byName.set(s.name, s);
146
+ }
147
+ return [...byName.values()];
148
+ }
149
+ /** Skills that apply to a given tier (strong drops `scaffolding`). */
150
+ forTier(tier) {
151
+ return this.load().filter((s) => {
152
+ if (s.tier !== "all" && s.tier !== tier)
153
+ return false;
154
+ if (tier === "strong" && s.kind === "scaffolding")
155
+ return false;
156
+ return true;
157
+ });
158
+ }
159
+ /** Assemble a compact standing-instructions block for a tier (capped). */
160
+ contextBlock(tier) {
161
+ const skills = this.forTier(tier);
162
+ if (skills.length === 0)
163
+ return "";
164
+ const parts = ["Standing instructions (skills):"];
165
+ let budget = this.maxChars;
166
+ for (const s of skills) {
167
+ const line = `- [${s.kind}] ${s.name}: ${s.content.trim()}`;
168
+ if (line.length > budget) {
169
+ parts.push(line.slice(0, Math.max(0, budget)) + "…");
170
+ break;
171
+ }
172
+ parts.push(line);
173
+ budget -= line.length;
174
+ }
175
+ return parts.join("\n");
176
+ }
177
+ readDir(dir) {
178
+ if (!existsSync(dir))
179
+ return [];
180
+ let entries;
181
+ try {
182
+ entries = readdirSync(dir);
183
+ }
184
+ catch {
185
+ return [];
186
+ }
187
+ const out = [];
188
+ for (const e of entries) {
189
+ if (!e.toLowerCase().endsWith(".md"))
190
+ continue;
191
+ const s = this.readFile(join(dir, e), e.replace(/\.md$/i, ""));
192
+ if (s)
193
+ out.push(s);
194
+ }
195
+ return out;
196
+ }
197
+ readFile(path, name) {
198
+ if (!existsSync(path))
199
+ return null;
200
+ try {
201
+ if (!statSync(path).isFile())
202
+ return null;
203
+ const raw = readFileSync(path, "utf8").slice(0, MAX_FILE_CHARS);
204
+ const { tier, kind, body } = parseFrontMatter(raw);
205
+ return { name, path, tier, kind, content: body };
206
+ }
207
+ catch {
208
+ return null;
209
+ }
210
+ }
211
+ }
212
+ function parseFrontMatter(raw) {
213
+ let tier = "all";
214
+ let kind = "context";
215
+ const lines = raw.split(/\r?\n/);
216
+ let consumed = 0;
217
+ for (let i = 0; i < Math.min(lines.length, 6); i++) {
218
+ const m = /^\s*maq-(tier|kind)\s*:\s*(\w+)/i.exec(lines[i]);
219
+ if (!m)
220
+ break;
221
+ const key = m[1].toLowerCase();
222
+ const val = m[2].toLowerCase();
223
+ if (key === "tier" && (val === "cheap" || val === "strong" || val === "all"))
224
+ tier = val;
225
+ if (key === "kind" && (val === "constraint" || val === "scaffolding" || val === "context"))
226
+ kind = val;
227
+ consumed = i + 1;
228
+ }
229
+ return { tier, kind, body: lines.slice(consumed).join("\n").trim() };
230
+ }
231
+ /**
232
+ * Scaffold starter skill files (and an AGENTS.md if absent) into a project so
233
+ * teams can extend the built-in pack. Never overwrites existing files.
234
+ * Returns the list of files written.
235
+ */
236
+ export function initSkills(cwd, skillsDir = ".maq/skills") {
237
+ const written = [];
238
+ const dir = join(cwd, skillsDir);
239
+ try {
240
+ mkdirSync(dir, { recursive: true });
241
+ }
242
+ catch {
243
+ /* ignore */
244
+ }
245
+ const example = join(dir, "example-skill.md");
246
+ if (!existsSync(example)) {
247
+ writeFileSync(example, [
248
+ "maq-tier: all",
249
+ "maq-kind: constraint",
250
+ "",
251
+ "# Example project skill",
252
+ "",
253
+ "Replace this with a real, single-purpose rule specific to THIS project —",
254
+ "something an agent cannot infer from the code (a convention, a boundary, a",
255
+ "gotcha). Keep it concise; stale or contradictory rules corrupt decisions.",
256
+ "",
257
+ "Front-matter options (first lines): `maq-tier: cheap|strong|all`,",
258
+ "`maq-kind: constraint|scaffolding|context`. `scaffolding` skills are",
259
+ "dropped for the strong model tier.",
260
+ ].join("\n"), "utf8");
261
+ written.push(example);
262
+ }
263
+ const agents = join(cwd, "AGENTS.md");
264
+ if (!existsSync(agents)) {
265
+ writeFileSync(agents, [
266
+ "# AGENTS.md",
267
+ "",
268
+ "Standing instructions for coding agents working in this repo. Keep it tight —",
269
+ "models reliably follow ~150–200 instructions before adherence degrades, so",
270
+ "write only what an agent cannot discover from the code itself.",
271
+ "",
272
+ "## Commands",
273
+ "- build: (fill in)",
274
+ "- test: (fill in)",
275
+ "- lint: (fill in)",
276
+ "",
277
+ "## Conventions",
278
+ "- (language/style/library conventions specific to this repo)",
279
+ "",
280
+ "## Boundaries",
281
+ "- (directories or actions that are off-limits)",
282
+ "",
283
+ "## MAQ — lessons from verification failures",
284
+ "(MAQ appends lessons here automatically when a run fails verification.)",
285
+ ].join("\n"), "utf8");
286
+ written.push(agents);
287
+ }
288
+ return written;
289
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Sub-agent isolation (containment over delegation).
3
+ *
4
+ * The lead/orchestrator should NOT inherit a sub-agent's full working set. A
5
+ * sub-agent runs in a FRESH, minimal context (task + only the explicitly passed
6
+ * artifacts) and returns a CONCISE summary — findings, decisions, artifacts —
7
+ * not a raw transcript. This is what makes multi-step work cheap: you pay for
8
+ * isolation only if you get the token savings of a summary back.
9
+ *
10
+ * This mirrors the mechanism a long-horizon model does internally (plan →
11
+ * delegate to sub-agents → synthesize), supplied externally so any model gets it.
12
+ */
13
+ import type { ModelProvider } from "./model.js";
14
+ import { CostMeter } from "./cost.js";
15
+ export interface SubagentInput {
16
+ /** The scoped sub-task. */
17
+ task: string;
18
+ /** Minimal, explicitly-selected context package (NOT the parent's whole state). */
19
+ context?: string;
20
+ /** Target summary size hint, in tokens. */
21
+ summaryTokens?: number;
22
+ }
23
+ export interface SubagentResult {
24
+ summary: string;
25
+ model: string;
26
+ provider: string;
27
+ promptTokensEst: number;
28
+ completionTokensEst: number;
29
+ costUsd: number;
30
+ }
31
+ /**
32
+ * Run one isolated sub-agent. Returns a condensed summary plus accounting.
33
+ * The parent should merge only the `summary` into its context.
34
+ */
35
+ export declare function runSubagent(input: SubagentInput, provider: ModelProvider, model: string, meter?: CostMeter): Promise<SubagentResult>;
36
+ /**
37
+ * Fan out several scoped sub-tasks in parallel and collect their summaries.
38
+ * Each runs in its own isolated context; the orchestrator sees only summaries.
39
+ */
40
+ export declare function runSubagents(inputs: SubagentInput[], provider: ModelProvider, model: string, meter?: CostMeter): Promise<SubagentResult[]>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Sub-agent isolation (containment over delegation).
3
+ *
4
+ * The lead/orchestrator should NOT inherit a sub-agent's full working set. A
5
+ * sub-agent runs in a FRESH, minimal context (task + only the explicitly passed
6
+ * artifacts) and returns a CONCISE summary — findings, decisions, artifacts —
7
+ * not a raw transcript. This is what makes multi-step work cheap: you pay for
8
+ * isolation only if you get the token savings of a summary back.
9
+ *
10
+ * This mirrors the mechanism a long-horizon model does internally (plan →
11
+ * delegate to sub-agents → synthesize), supplied externally so any model gets it.
12
+ */
13
+ const SYSTEM = "You are an isolated sub-agent invoked by an orchestrator. You have a single scoped task. " +
14
+ "Explore/answer it using only the context provided. Return a CONCISE structured summary " +
15
+ "(findings, decisions, artifacts, open questions) — never a raw transcript or full data dump. " +
16
+ "Containment rule: hand back a concise answer, not everything you looked at.";
17
+ /**
18
+ * Run one isolated sub-agent. Returns a condensed summary plus accounting.
19
+ * The parent should merge only the `summary` into its context.
20
+ */
21
+ export async function runSubagent(input, provider, model, meter) {
22
+ const summaryTokens = input.summaryTokens ?? 1500;
23
+ const user = [
24
+ `Sub-task: ${input.task}`,
25
+ input.context ? `\nContext package (only what you were given):\n${input.context}` : "",
26
+ `\nReturn at most ~${summaryTokens} tokens. Structure: Findings / Decisions / Artifacts / Open questions.`,
27
+ ]
28
+ .filter(Boolean)
29
+ .join("\n");
30
+ const res = await provider.complete({
31
+ model,
32
+ tier: "cheap",
33
+ maxTokens: summaryTokens,
34
+ messages: [
35
+ { role: "system", content: SYSTEM },
36
+ { role: "user", content: user },
37
+ ],
38
+ });
39
+ meter?.record(res.model, res.promptTokensEst, res.completionTokensEst);
40
+ return {
41
+ summary: res.text.trim(),
42
+ model: res.model,
43
+ provider: res.provider,
44
+ promptTokensEst: res.promptTokensEst,
45
+ completionTokensEst: res.completionTokensEst,
46
+ costUsd: res.costUsd ?? 0,
47
+ };
48
+ }
49
+ /**
50
+ * Fan out several scoped sub-tasks in parallel and collect their summaries.
51
+ * Each runs in its own isolated context; the orchestrator sees only summaries.
52
+ */
53
+ export async function runSubagents(inputs, provider, model, meter) {
54
+ return Promise.all(inputs.map((i) => runSubagent(i, provider, model, meter)));
55
+ }