@agentplate/cli 1.1.0 → 1.3.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/CHANGELOG.md +46 -0
- package/agents/coordinator.md +6 -0
- package/agents/lead.md +3 -1
- package/package.json +1 -1
- package/src/agents/capacity.test.ts +55 -0
- package/src/agents/capacity.ts +50 -0
- package/src/agents/drive.test.ts +155 -0
- package/src/agents/drive.ts +270 -0
- package/src/agents/turn-runner.test.ts +67 -0
- package/src/agents/turn-runner.ts +18 -1
- package/src/commands/sling.ts +46 -117
- package/src/commands/turn.test.ts +101 -0
- package/src/commands/turn.ts +88 -0
- package/src/commands/watch.test.ts +136 -0
- package/src/commands/watch.ts +151 -0
- package/src/config.test.ts +32 -0
- package/src/config.ts +16 -1
- package/src/errors.ts +11 -0
- package/src/index.ts +4 -0
- package/src/insights/quality-gates.test.ts +43 -0
- package/src/insights/quality-gates.ts +30 -31
- package/src/merge/auto.test.ts +157 -0
- package/src/merge/auto.ts +118 -0
- package/src/runtimes/registry.test.ts +16 -2
- package/src/runtimes/registry.ts +13 -0
- package/src/runtimes/resolve.test.ts +49 -0
- package/src/runtimes/resolve.ts +11 -7
- package/src/sessions/store.test.ts +13 -0
- package/src/sessions/store.ts +20 -0
- package/src/types.ts +30 -1
- package/src/version.ts +1 -1
- package/src/wizard/setup.test.ts +45 -0
- package/src/wizard/setup.ts +181 -2
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { ValidationError } from "../errors.ts";
|
|
3
|
-
import type { ResolvedModel } from "../types.ts";
|
|
3
|
+
import type { ResolvedModel, RuntimeConfig } from "../types.ts";
|
|
4
4
|
import { ClaudeRuntime } from "./claude.ts";
|
|
5
5
|
import { CodexRuntime } from "./codex.ts";
|
|
6
6
|
import { CursorRuntime } from "./cursor.ts";
|
|
7
7
|
import { GeminiRuntime } from "./gemini.ts";
|
|
8
8
|
import { MockRuntime } from "./mock.ts";
|
|
9
9
|
import { OpenCodeRuntime, opencodeModel } from "./opencode.ts";
|
|
10
|
-
import { getRuntime, getRuntimeNames } from "./registry.ts";
|
|
10
|
+
import { getRuntime, getRuntimeNames, runtimeNameForCapability } from "./registry.ts";
|
|
11
11
|
import type { AgentEvent, DirectSpawnOpts } from "./types.ts";
|
|
12
12
|
|
|
13
|
+
describe("runtimeNameForCapability", () => {
|
|
14
|
+
const rt: RuntimeConfig = { default: "claude", capabilities: { scout: "codex" } };
|
|
15
|
+
test("explicit override wins over everything", () => {
|
|
16
|
+
expect(runtimeNameForCapability(rt, "scout", "gemini")).toBe("gemini");
|
|
17
|
+
});
|
|
18
|
+
test("per-capability override applies when set", () => {
|
|
19
|
+
expect(runtimeNameForCapability(rt, "scout")).toBe("codex");
|
|
20
|
+
});
|
|
21
|
+
test("falls back to the default runtime", () => {
|
|
22
|
+
expect(runtimeNameForCapability(rt, "builder")).toBe("claude");
|
|
23
|
+
expect(runtimeNameForCapability({ default: "opencode" }, "scout")).toBe("opencode");
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
13
27
|
// Minimal DirectSpawnOpts builder for argv-shape assertions. `cwd` and
|
|
14
28
|
// `instructionPath` are required by the type but irrelevant to argv here.
|
|
15
29
|
function spawnOpts(overrides: Partial<DirectSpawnOpts> = {}): DirectSpawnOpts {
|
package/src/runtimes/registry.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { ValidationError } from "../errors.ts";
|
|
11
|
+
import type { Capability, RuntimeConfig } from "../types.ts";
|
|
11
12
|
import { ClaudeRuntime } from "./claude.ts";
|
|
12
13
|
import { CodexRuntime } from "./codex.ts";
|
|
13
14
|
import { CursorRuntime } from "./cursor.ts";
|
|
@@ -54,6 +55,18 @@ export function getRuntime(name?: string, fallback?: string): AgentRuntime {
|
|
|
54
55
|
return factory();
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Resolve which runtime adapter drives a given capability: an explicit `override`
|
|
60
|
+
* wins, then a per-capability entry in `runtime.capabilities`, then the default.
|
|
61
|
+
*/
|
|
62
|
+
export function runtimeNameForCapability(
|
|
63
|
+
runtime: RuntimeConfig,
|
|
64
|
+
capability: Capability,
|
|
65
|
+
override?: string,
|
|
66
|
+
): string {
|
|
67
|
+
return override ?? runtime.capabilities?.[capability] ?? runtime.default;
|
|
68
|
+
}
|
|
69
|
+
|
|
57
70
|
/**
|
|
58
71
|
* Names of all registered runtimes, in registration order. Used to validate a
|
|
59
72
|
* user-supplied runtime name and to render the choices in help / errors.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for resolveModel — focused on per-capability model tiering. Auth-mode is
|
|
3
|
+
* set to "none" so no secret is read; we assert which model id is chosen.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import { DEFAULT_CONFIG } from "../config.ts";
|
|
8
|
+
import type { AgentplateConfig, Capability } from "../types.ts";
|
|
9
|
+
import { resolveModel } from "./resolve.ts";
|
|
10
|
+
|
|
11
|
+
/** A config whose active provider has a default model + optional tiering map. */
|
|
12
|
+
function configWith(
|
|
13
|
+
model: string | undefined,
|
|
14
|
+
models?: Partial<Record<Capability, string>>,
|
|
15
|
+
): AgentplateConfig {
|
|
16
|
+
const cfg = structuredClone(DEFAULT_CONFIG);
|
|
17
|
+
cfg.activeProvider = "anthropic";
|
|
18
|
+
cfg.providers.anthropic = { type: "native", authMode: "none", model, models };
|
|
19
|
+
return cfg;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ROOT = "/tmp/agentplate-resolve-test"; // never read (authMode "none")
|
|
23
|
+
|
|
24
|
+
describe("resolveModel — tiering", () => {
|
|
25
|
+
test("uses the provider default model when no capability is given", () => {
|
|
26
|
+
const { model } = resolveModel(configWith("claude-opus-4-8"), ROOT, "alias");
|
|
27
|
+
expect(model).toBe("claude-opus-4-8");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("a per-capability override wins over the provider default", () => {
|
|
31
|
+
const cfg = configWith("claude-opus-4-8", {
|
|
32
|
+
scout: "claude-haiku-4-5",
|
|
33
|
+
reviewer: "claude-haiku-4-5",
|
|
34
|
+
});
|
|
35
|
+
expect(resolveModel(cfg, ROOT, "alias", "scout").model).toBe("claude-haiku-4-5");
|
|
36
|
+
expect(resolveModel(cfg, ROOT, "alias", "reviewer").model).toBe("claude-haiku-4-5");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("a capability without an override falls back to the provider default", () => {
|
|
40
|
+
const cfg = configWith("claude-opus-4-8", { scout: "claude-haiku-4-5" });
|
|
41
|
+
expect(resolveModel(cfg, ROOT, "alias", "builder").model).toBe("claude-opus-4-8");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("falls back to the manifest alias when the provider has no model", () => {
|
|
45
|
+
expect(resolveModel(configWith(undefined), ROOT, "manifest-alias", "builder").model).toBe(
|
|
46
|
+
"manifest-alias",
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
});
|
package/src/runtimes/resolve.ts
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Model resolution — bridge between the agent manifest (which names models by
|
|
3
3
|
* alias) and a concrete {@link ResolvedModel} the runtime can spawn.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* The active provider's configured `model` (set by `agentplate setup`) is the
|
|
6
|
+
* concrete model, and the provider's `authTokenEnv` secret is injected as an env
|
|
7
|
+
* var. Per-capability **model tiering** is supported: when a `capability` is given
|
|
8
|
+
* and the provider has a `models[capability]` override (e.g. a fast model for
|
|
9
|
+
* `scout`/`reviewer`), that wins; otherwise the provider `model`, otherwise the
|
|
10
|
+
* manifest alias.
|
|
10
11
|
*
|
|
11
12
|
* Auth mode matters here: only `api-key`/`env` providers inject a credential env
|
|
12
13
|
* var. `subscription` providers delegate to the runtime CLI's own login (e.g. a
|
|
@@ -15,7 +16,7 @@
|
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
import { getSecret } from "../secrets.ts";
|
|
18
|
-
import type { AgentplateConfig, ResolvedModel } from "../types.ts";
|
|
19
|
+
import type { AgentplateConfig, Capability, ResolvedModel } from "../types.ts";
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Resolve the concrete model + provider env for a manifest model alias.
|
|
@@ -23,14 +24,17 @@ import type { AgentplateConfig, ResolvedModel } from "../types.ts";
|
|
|
23
24
|
* @param config loaded Agentplate config
|
|
24
25
|
* @param root project root (to read the gitignored secret value)
|
|
25
26
|
* @param manifestModel the model alias/id from the agent definition
|
|
27
|
+
* @param capability optional agent capability for per-capability model tiering
|
|
26
28
|
*/
|
|
27
29
|
export function resolveModel(
|
|
28
30
|
config: AgentplateConfig,
|
|
29
31
|
root: string,
|
|
30
32
|
manifestModel: string,
|
|
33
|
+
capability?: Capability,
|
|
31
34
|
): ResolvedModel {
|
|
32
35
|
const provider = config.providers[config.activeProvider];
|
|
33
|
-
const
|
|
36
|
+
const tiered = capability ? provider?.models?.[capability] : undefined;
|
|
37
|
+
const model = tiered ?? provider?.model ?? manifestModel;
|
|
34
38
|
const env: Record<string, string> = {};
|
|
35
39
|
|
|
36
40
|
// Default to "api-key" for legacy configs written before authMode existed.
|
|
@@ -318,6 +318,19 @@ describe("createSessionStore — countActive", () => {
|
|
|
318
318
|
store.updateSessionState(s.id, "completed");
|
|
319
319
|
expect(store.countActive("run-x")).toBe(0);
|
|
320
320
|
});
|
|
321
|
+
|
|
322
|
+
test("countActiveByParent counts a parent's active children only", () => {
|
|
323
|
+
store.upsertSession(makeSession({ runId: "r", parentAgent: "lead-1", state: "working" }));
|
|
324
|
+
store.upsertSession(makeSession({ runId: "r", parentAgent: "lead-1", state: "booting" }));
|
|
325
|
+
const done = makeSession({ runId: "r", parentAgent: "lead-1", state: "completed" });
|
|
326
|
+
store.upsertSession(done);
|
|
327
|
+
store.upsertSession(makeSession({ runId: "r", parentAgent: "lead-2", state: "working" }));
|
|
328
|
+
|
|
329
|
+
expect(store.countActiveByParent("lead-1", "r")).toBe(2); // excludes the completed child
|
|
330
|
+
expect(store.countActiveByParent("lead-2", "r")).toBe(1);
|
|
331
|
+
expect(store.countActiveByParent("lead-1", "other-run")).toBe(0);
|
|
332
|
+
expect(store.countActiveByParent("nobody")).toBe(0);
|
|
333
|
+
});
|
|
321
334
|
});
|
|
322
335
|
|
|
323
336
|
describe("createSessionStore — file-backed database", () => {
|
package/src/sessions/store.ts
CHANGED
|
@@ -57,6 +57,7 @@ export interface SessionStore {
|
|
|
57
57
|
setRuntimeSessionId(id: string, runtimeSessionId: string): void;
|
|
58
58
|
touch(id: string): void;
|
|
59
59
|
countActive(runId?: string): number;
|
|
60
|
+
countActiveByParent(parentAgent: string, runId?: string): number;
|
|
60
61
|
// --- Lifecycle ---
|
|
61
62
|
close(): void;
|
|
62
63
|
}
|
|
@@ -328,6 +329,24 @@ export function createSessionStore(dbPath: string): SessionStore {
|
|
|
328
329
|
return row.n;
|
|
329
330
|
}
|
|
330
331
|
|
|
332
|
+
function countActiveByParent(parentAgent: string, runId?: string): number {
|
|
333
|
+
// Active children of a given parent (for the per-lead child cap).
|
|
334
|
+
if (runId !== undefined) {
|
|
335
|
+
const row = db
|
|
336
|
+
.query(
|
|
337
|
+
`SELECT COUNT(*) AS n FROM sessions WHERE parent_agent = ? AND run_id = ? AND state IN ${ACTIVE_STATES_SQL}`,
|
|
338
|
+
)
|
|
339
|
+
.get(parentAgent, runId) as { n: number };
|
|
340
|
+
return row.n;
|
|
341
|
+
}
|
|
342
|
+
const row = db
|
|
343
|
+
.query(
|
|
344
|
+
`SELECT COUNT(*) AS n FROM sessions WHERE parent_agent = ? AND state IN ${ACTIVE_STATES_SQL}`,
|
|
345
|
+
)
|
|
346
|
+
.get(parentAgent) as { n: number };
|
|
347
|
+
return row.n;
|
|
348
|
+
}
|
|
349
|
+
|
|
331
350
|
function close(): void {
|
|
332
351
|
db.close();
|
|
333
352
|
}
|
|
@@ -345,6 +364,7 @@ export function createSessionStore(dbPath: string): SessionStore {
|
|
|
345
364
|
setRuntimeSessionId,
|
|
346
365
|
touch,
|
|
347
366
|
countActive,
|
|
367
|
+
countActiveByParent,
|
|
348
368
|
close,
|
|
349
369
|
};
|
|
350
370
|
}
|
package/src/types.ts
CHANGED
|
@@ -127,6 +127,11 @@ export interface ProviderConfig {
|
|
|
127
127
|
authTokenEnv?: string;
|
|
128
128
|
/** Default model id for this provider. */
|
|
129
129
|
model?: string;
|
|
130
|
+
/**
|
|
131
|
+
* Per-capability model overrides (tiering): e.g. a fast/cheap model for
|
|
132
|
+
* `scout`/`reviewer` and a strong model for `builder`. Falls back to `model`.
|
|
133
|
+
*/
|
|
134
|
+
models?: Partial<Record<Capability, string>>;
|
|
130
135
|
}
|
|
131
136
|
|
|
132
137
|
/** Orchestration limits and agent registry locations. */
|
|
@@ -148,12 +153,36 @@ export interface AgentsConfig {
|
|
|
148
153
|
* `0` disables idle reaping. Default 10.
|
|
149
154
|
*/
|
|
150
155
|
idleTimeoutMinutes: number;
|
|
156
|
+
/**
|
|
157
|
+
* Hard wall-clock cap on a SINGLE turn, in minutes. Unlike idle reaping (which
|
|
158
|
+
* needs inactivity), this kills a turn that keeps streaming but never finishes.
|
|
159
|
+
* `0` disables the cap. Default 0.
|
|
160
|
+
*/
|
|
161
|
+
turnTimeoutMinutes: number;
|
|
162
|
+
/** Default: leads skip the scout step (go straight to builders). */
|
|
163
|
+
skipScout: boolean;
|
|
164
|
+
/** Default: leads skip the reviewer step before integrating. */
|
|
165
|
+
skipReview: boolean;
|
|
166
|
+
/** Skip the post-turn quality-gate run (speed; disables `on-gates-pass` merge). */
|
|
167
|
+
skipGates: boolean;
|
|
168
|
+
/** Skip the post-turn skill-distillation loop. */
|
|
169
|
+
skipSkills: boolean;
|
|
151
170
|
}
|
|
152
171
|
|
|
153
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* When a completed worker's branch is auto-merged into the canonical branch.
|
|
174
|
+
* - `off`: never (the operator/coordinator merges manually) — the default.
|
|
175
|
+
* - `on-gates-pass`: merge only if the task's quality gates pass.
|
|
176
|
+
* - `on-complete`: merge as soon as the agent finishes without error.
|
|
177
|
+
*/
|
|
178
|
+
export type AutoMergeMode = "off" | "on-gates-pass" | "on-complete";
|
|
179
|
+
|
|
180
|
+
/** Conflict-resolution and auto-merge behavior for merging agent branches. */
|
|
154
181
|
export interface MergeConfig {
|
|
155
182
|
/** Allow AI-assisted resolution of semantic conflicts. */
|
|
156
183
|
aiResolveEnabled: boolean;
|
|
184
|
+
/** When to auto-merge a completed worker's branch into the canonical branch. */
|
|
185
|
+
autoMerge: AutoMergeMode;
|
|
157
186
|
}
|
|
158
187
|
|
|
159
188
|
/** Logging behavior. */
|
package/src/version.ts
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wizard helper tests. The interactive prompts themselves need a TTY, but the
|
|
3
|
+
* pure pieces are unit-tested directly. `detectQualityGates` reads a project's
|
|
4
|
+
* package.json to suggest gate commands.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
8
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { detectQualityGates } from "./setup.ts";
|
|
12
|
+
|
|
13
|
+
let dir: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
dir = mkdtempSync(join(tmpdir(), "agentplate-wizard-"));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
rmSync(dir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("detectQualityGates", () => {
|
|
24
|
+
test("falls back to stack defaults when there is no package.json", () => {
|
|
25
|
+
const gates = detectQualityGates(dir);
|
|
26
|
+
expect(gates).toEqual([
|
|
27
|
+
{ name: "test", command: "bun test" },
|
|
28
|
+
{ name: "lint", command: "biome check ." },
|
|
29
|
+
{ name: "typecheck", command: "tsc --noEmit" },
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("prefers `bun run <script>` for scripts the project defines", () => {
|
|
34
|
+
writeFileSync(
|
|
35
|
+
join(dir, "package.json"),
|
|
36
|
+
JSON.stringify({ scripts: { test: "vitest", typecheck: "tsc -p ." } }),
|
|
37
|
+
"utf8",
|
|
38
|
+
);
|
|
39
|
+
const gates = detectQualityGates(dir);
|
|
40
|
+
const byName = Object.fromEntries(gates.map((g) => [g.name, g.command]));
|
|
41
|
+
expect(byName.test).toBe("bun run test"); // script present → run it
|
|
42
|
+
expect(byName.typecheck).toBe("bun run typecheck"); // script present
|
|
43
|
+
expect(byName.lint).toBe("biome check ."); // no lint script → fallback
|
|
44
|
+
});
|
|
45
|
+
});
|
package/src/wizard/setup.ts
CHANGED
|
@@ -7,10 +7,18 @@
|
|
|
7
7
|
* module owns only the interactive I/O.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
10
12
|
import * as p from "@clack/prompts";
|
|
11
13
|
import { applyProviderSelection, buildProviderConfig } from "../providers/apply.ts";
|
|
12
14
|
import { getProviderSpec, listProviders, meetsContextFloor } from "../providers/registry.ts";
|
|
13
|
-
import type {
|
|
15
|
+
import type {
|
|
16
|
+
AgentplateConfig,
|
|
17
|
+
AuthMode,
|
|
18
|
+
AutoMergeMode,
|
|
19
|
+
Capability,
|
|
20
|
+
QualityGate,
|
|
21
|
+
} from "../types.ts";
|
|
14
22
|
import { commandOnPath, detectDefaultRuntime } from "../utils/detect.ts";
|
|
15
23
|
|
|
16
24
|
/** Runtimes the wizard can offer. */
|
|
@@ -34,6 +42,32 @@ export interface WizardResult {
|
|
|
34
42
|
secret?: { key: string; value: string };
|
|
35
43
|
}
|
|
36
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Suggest quality gates for a project. Prefers the project's own package.json
|
|
47
|
+
* scripts (`bun run <script>`) when present, falling back to the Bun/Biome/TS
|
|
48
|
+
* defaults this stack uses. The user confirms/deselects in the wizard.
|
|
49
|
+
*/
|
|
50
|
+
export function detectQualityGates(root: string): QualityGate[] {
|
|
51
|
+
let scripts: Record<string, string> = {};
|
|
52
|
+
try {
|
|
53
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
|
|
54
|
+
scripts?: Record<string, string>;
|
|
55
|
+
};
|
|
56
|
+
scripts = pkg.scripts ?? {};
|
|
57
|
+
} catch {
|
|
58
|
+
// No package.json (or unreadable) — fall back to stack defaults below.
|
|
59
|
+
}
|
|
60
|
+
const gate = (name: string, fallback: string): QualityGate => ({
|
|
61
|
+
name,
|
|
62
|
+
command: scripts[name] ? `bun run ${name}` : fallback,
|
|
63
|
+
});
|
|
64
|
+
return [
|
|
65
|
+
gate("test", "bun test"),
|
|
66
|
+
gate("lint", "biome check ."),
|
|
67
|
+
gate("typecheck", "tsc --noEmit"),
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
37
71
|
/** Abort the wizard cleanly if the user cancelled a prompt. */
|
|
38
72
|
function ensure<T>(value: T | symbol): T {
|
|
39
73
|
if (p.isCancel(value)) {
|
|
@@ -192,6 +226,31 @@ export async function runSetupWizard(currentConfig: AgentplateConfig): Promise<W
|
|
|
192
226
|
).trim();
|
|
193
227
|
}
|
|
194
228
|
|
|
229
|
+
// 4b. Model tiering — optionally use a faster/cheaper model for read-only
|
|
230
|
+
// roles (scout, reviewer), keeping the chosen model for the rest.
|
|
231
|
+
let modelsByCapability: Partial<Record<Capability, string>> | undefined;
|
|
232
|
+
if (eligible.length > 1) {
|
|
233
|
+
const wantTiering = ensure(
|
|
234
|
+
await p.confirm({
|
|
235
|
+
message: "Use a faster/cheaper model for read-only roles (scout & reviewer)?",
|
|
236
|
+
initialValue: false,
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
if (wantTiering) {
|
|
240
|
+
const fast = ensure(
|
|
241
|
+
await p.select({
|
|
242
|
+
message: "Fast model for scout & reviewer",
|
|
243
|
+
options: eligible.map((m) => ({
|
|
244
|
+
value: m.id,
|
|
245
|
+
label: m.label,
|
|
246
|
+
hint: `${Math.round(m.contextWindow / 1000)}k context`,
|
|
247
|
+
})),
|
|
248
|
+
}),
|
|
249
|
+
) as string;
|
|
250
|
+
modelsByCapability = { scout: fast, reviewer: fast };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
195
254
|
// 5. Runtime -----------------------------------------------------------
|
|
196
255
|
const detected = await detectDefaultRuntime();
|
|
197
256
|
const installed = new Set<string>();
|
|
@@ -219,7 +278,115 @@ export async function runSetupWizard(currentConfig: AgentplateConfig): Promise<W
|
|
|
219
278
|
}),
|
|
220
279
|
);
|
|
221
280
|
|
|
222
|
-
// 6.
|
|
281
|
+
// 6. Orchestration & merge --------------------------------------------
|
|
282
|
+
const canonicalBranch = currentConfig.project.canonicalBranch;
|
|
283
|
+
const suggestedGates = detectQualityGates(currentConfig.project.root || ".");
|
|
284
|
+
const chosenGateNames = ensure(
|
|
285
|
+
await p.multiselect({
|
|
286
|
+
message:
|
|
287
|
+
"Quality gates to run on a worker's output (used by skill distillation and 'on gates pass' auto-merge)",
|
|
288
|
+
options: suggestedGates.map((g) => ({ value: g.name, label: g.name, hint: g.command })),
|
|
289
|
+
initialValues: suggestedGates.map((g) => g.name),
|
|
290
|
+
required: false,
|
|
291
|
+
}),
|
|
292
|
+
) as string[];
|
|
293
|
+
const qualityGates = suggestedGates.filter((g) => chosenGateNames.includes(g.name));
|
|
294
|
+
|
|
295
|
+
const autoMerge = ensure(
|
|
296
|
+
await p.select({
|
|
297
|
+
message: `Auto-merge a worker's branch into ${canonicalBranch} when it finishes?`,
|
|
298
|
+
initialValue: "off",
|
|
299
|
+
options: [
|
|
300
|
+
{ value: "off", label: "Never — merge manually", hint: "default; safest" },
|
|
301
|
+
{
|
|
302
|
+
value: "on-gates-pass",
|
|
303
|
+
label: "On quality gates pass",
|
|
304
|
+
hint: qualityGates.length
|
|
305
|
+
? "merge only when gates are green"
|
|
306
|
+
: "needs gates — will hold otherwise",
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
value: "on-complete",
|
|
310
|
+
label: "On complete",
|
|
311
|
+
hint: "merge as soon as the agent finishes (no gate check)",
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
}),
|
|
315
|
+
) as AutoMergeMode;
|
|
316
|
+
|
|
317
|
+
if (autoMerge === "on-gates-pass" && qualityGates.length === 0) {
|
|
318
|
+
p.note(
|
|
319
|
+
"No quality gates selected, so 'on gates pass' will hold every merge for manual review.\nAdd gates or pick a different mode to actually auto-merge.",
|
|
320
|
+
"Heads up",
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 6b. Advanced limits — gated so the common path stays short.
|
|
325
|
+
const agents = { ...currentConfig.agents };
|
|
326
|
+
const tuneAdvanced = ensure(
|
|
327
|
+
await p.confirm({
|
|
328
|
+
message: "Tune advanced limits (concurrency, timeouts, skip steps)?",
|
|
329
|
+
initialValue: false,
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
if (tuneAdvanced) {
|
|
333
|
+
const posInt = (v: string | undefined): string | undefined =>
|
|
334
|
+
v && Number.isInteger(Number(v)) && Number(v) >= 0 ? undefined : "Enter a whole number ≥ 0";
|
|
335
|
+
agents.maxConcurrent = Number(
|
|
336
|
+
ensure(
|
|
337
|
+
await p.text({
|
|
338
|
+
message: "Max agents running at once",
|
|
339
|
+
initialValue: String(agents.maxConcurrent),
|
|
340
|
+
validate: (v) => (v && Number(v) >= 1 ? undefined : "Enter a number ≥ 1"),
|
|
341
|
+
}),
|
|
342
|
+
),
|
|
343
|
+
);
|
|
344
|
+
agents.maxAgentsPerLead = Number(
|
|
345
|
+
ensure(
|
|
346
|
+
await p.text({
|
|
347
|
+
message: "Max workers per lead",
|
|
348
|
+
initialValue: String(agents.maxAgentsPerLead),
|
|
349
|
+
validate: (v) => (v && Number(v) >= 1 ? undefined : "Enter a number ≥ 1"),
|
|
350
|
+
}),
|
|
351
|
+
),
|
|
352
|
+
);
|
|
353
|
+
agents.turnTimeoutMinutes = Number(
|
|
354
|
+
ensure(
|
|
355
|
+
await p.text({
|
|
356
|
+
message: "Per-turn timeout in minutes (0 = no cap)",
|
|
357
|
+
initialValue: String(agents.turnTimeoutMinutes),
|
|
358
|
+
validate: posInt,
|
|
359
|
+
}),
|
|
360
|
+
),
|
|
361
|
+
);
|
|
362
|
+
const skips = ensure(
|
|
363
|
+
await p.multiselect({
|
|
364
|
+
message: "Default speed shortcuts (leave empty for none)",
|
|
365
|
+
options: [
|
|
366
|
+
{
|
|
367
|
+
value: "skipScout",
|
|
368
|
+
label: "Skip scout step",
|
|
369
|
+
hint: "leads dispatch builders directly",
|
|
370
|
+
},
|
|
371
|
+
{ value: "skipReview", label: "Skip review step", hint: "no reviewer before integrate" },
|
|
372
|
+
{
|
|
373
|
+
value: "skipGates",
|
|
374
|
+
label: "Skip quality gates",
|
|
375
|
+
hint: "faster; disables on-gates-pass merge",
|
|
376
|
+
},
|
|
377
|
+
{ value: "skipSkills", label: "Skip skill distillation" },
|
|
378
|
+
],
|
|
379
|
+
initialValues: [],
|
|
380
|
+
required: false,
|
|
381
|
+
}),
|
|
382
|
+
) as string[];
|
|
383
|
+
agents.skipScout = skips.includes("skipScout");
|
|
384
|
+
agents.skipReview = skips.includes("skipReview");
|
|
385
|
+
agents.skipGates = skips.includes("skipGates");
|
|
386
|
+
agents.skipSkills = skips.includes("skipSkills");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// 7. Summary -----------------------------------------------------------
|
|
223
390
|
const previewProvider = buildProviderConfig(spec, model, authMode, baseUrl);
|
|
224
391
|
const authSummary: Record<AuthMode, string> = {
|
|
225
392
|
subscription: `subscription / ${runtimeCli(spec.subscriptionRuntime ?? runtime)} login (no key stored)`,
|
|
@@ -234,6 +401,11 @@ export async function runSetupWizard(currentConfig: AgentplateConfig): Promise<W
|
|
|
234
401
|
`runtime: ${runtime}`,
|
|
235
402
|
`auth: ${authSummary[authMode]}`,
|
|
236
403
|
previewProvider.baseUrl ? `base URL: ${previewProvider.baseUrl}` : undefined,
|
|
404
|
+
`gates: ${qualityGates.length ? qualityGates.map((g) => g.name).join(", ") : "none"}`,
|
|
405
|
+
`auto-merge:${autoMerge}`,
|
|
406
|
+
modelsByCapability?.scout
|
|
407
|
+
? `fast model: ${modelsByCapability.scout} (scout, reviewer)`
|
|
408
|
+
: undefined,
|
|
237
409
|
]
|
|
238
410
|
.filter(Boolean)
|
|
239
411
|
.join("\n"),
|
|
@@ -248,6 +420,13 @@ export async function runSetupWizard(currentConfig: AgentplateConfig): Promise<W
|
|
|
248
420
|
baseUrl,
|
|
249
421
|
runtime,
|
|
250
422
|
});
|
|
423
|
+
config.merge = { ...config.merge, autoMerge };
|
|
424
|
+
config.agents = agents;
|
|
425
|
+
if (qualityGates.length) config.project = { ...config.project, qualityGates };
|
|
426
|
+
if (modelsByCapability) {
|
|
427
|
+
const pc = config.providers[providerId];
|
|
428
|
+
if (pc) config.providers[providerId] = { ...pc, models: modelsByCapability };
|
|
429
|
+
}
|
|
251
430
|
|
|
252
431
|
p.outro("Ready to write configuration.");
|
|
253
432
|
return secret ? { config, secret } : { config };
|