@kodrunhq/opencode-autopilot 0.1.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +303 -1
- package/assets/commands/oc-configure.md +45 -0
- package/assets/logo.svg +35 -0
- package/bin/cli.ts +346 -0
- package/package.json +6 -2
- package/src/agents/index.ts +53 -19
- package/src/config.ts +114 -19
- package/src/index.ts +24 -5
- package/src/installer.ts +31 -2
- package/src/registry/diversity.ts +78 -0
- package/src/registry/doctor.ts +84 -0
- package/src/registry/model-groups.ts +163 -0
- package/src/registry/resolver.ts +74 -0
- package/src/registry/types.ts +91 -0
- package/src/tools/configure.ts +320 -0
- package/assets/agents/placeholder-agent.md +0 -13
- package/assets/commands/configure.md +0 -17
- package/src/tools/placeholder.ts +0 -11
package/src/index.ts
CHANGED
|
@@ -12,13 +12,13 @@ import {
|
|
|
12
12
|
import { fallbackDefaults } from "./orchestrator/fallback/fallback-config";
|
|
13
13
|
import { resolveChain } from "./orchestrator/fallback/resolve-chain";
|
|
14
14
|
import { ocConfidence } from "./tools/confidence";
|
|
15
|
+
import { ocConfigure, setAvailableProviders, setOpenCodeConfig } from "./tools/configure";
|
|
15
16
|
import { ocCreateAgent } from "./tools/create-agent";
|
|
16
17
|
import { ocCreateCommand } from "./tools/create-command";
|
|
17
18
|
import { ocCreateSkill } from "./tools/create-skill";
|
|
18
19
|
import { ocForensics } from "./tools/forensics";
|
|
19
20
|
import { ocOrchestrate } from "./tools/orchestrate";
|
|
20
21
|
import { ocPhase } from "./tools/phase";
|
|
21
|
-
import { ocPlaceholder } from "./tools/placeholder";
|
|
22
22
|
import { ocPlan } from "./tools/plan";
|
|
23
23
|
import { ocReview } from "./tools/review";
|
|
24
24
|
import { ocState } from "./tools/state";
|
|
@@ -34,6 +34,19 @@ const plugin: Plugin = async (input) => {
|
|
|
34
34
|
console.error("[opencode-autopilot] Asset installation errors:", installResult.errors);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// Discover available providers/models from OpenCode SDK
|
|
38
|
+
try {
|
|
39
|
+
const providerResponse = await client.provider.list({
|
|
40
|
+
query: { directory: process.cwd() },
|
|
41
|
+
});
|
|
42
|
+
const providerData = providerResponse.data;
|
|
43
|
+
if (providerData?.all) {
|
|
44
|
+
setAvailableProviders(providerData.all);
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// Provider discovery is best-effort; configure will show empty models
|
|
48
|
+
}
|
|
49
|
+
|
|
37
50
|
// Load config for first-load detection and fallback settings
|
|
38
51
|
const config = await loadConfig();
|
|
39
52
|
const fallbackConfig = config?.fallback ?? fallbackDefaults;
|
|
@@ -77,7 +90,9 @@ const plugin: Plugin = async (input) => {
|
|
|
77
90
|
const agentConfigs = openCodeConfig?.agent as
|
|
78
91
|
| Record<string, Record<string, unknown>>
|
|
79
92
|
| undefined;
|
|
80
|
-
|
|
93
|
+
// Per-agent fallback_models are populated by configHook from group/override config.
|
|
94
|
+
// resolveChain reads config.agent[agentName].fallback_models (tier 1).
|
|
95
|
+
return resolveChain(agentName ?? "", agentConfigs, undefined);
|
|
81
96
|
},
|
|
82
97
|
});
|
|
83
98
|
|
|
@@ -91,7 +106,7 @@ const plugin: Plugin = async (input) => {
|
|
|
91
106
|
|
|
92
107
|
return {
|
|
93
108
|
tool: {
|
|
94
|
-
|
|
109
|
+
oc_configure: ocConfigure,
|
|
95
110
|
oc_create_agent: ocCreateAgent,
|
|
96
111
|
oc_create_skill: ocCreateSkill,
|
|
97
112
|
oc_create_command: ocCreateCommand,
|
|
@@ -105,8 +120,11 @@ const plugin: Plugin = async (input) => {
|
|
|
105
120
|
},
|
|
106
121
|
event: async ({ event }) => {
|
|
107
122
|
if (event.type === "session.created" && isFirstLoad(config)) {
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
await sdkOps.showToast(
|
|
124
|
+
"Welcome to OpenCode Autopilot!",
|
|
125
|
+
"Run /oc-configure to set up your model assignments for each agent group.",
|
|
126
|
+
"info",
|
|
127
|
+
);
|
|
110
128
|
}
|
|
111
129
|
|
|
112
130
|
// Fallback event handling (runs for all events)
|
|
@@ -116,6 +134,7 @@ const plugin: Plugin = async (input) => {
|
|
|
116
134
|
},
|
|
117
135
|
config: async (cfg: Config) => {
|
|
118
136
|
openCodeConfig = cfg;
|
|
137
|
+
setOpenCodeConfig(cfg);
|
|
119
138
|
await configHook(cfg);
|
|
120
139
|
},
|
|
121
140
|
"chat.message": async (
|
package/src/installer.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import { readdir } from "node:fs/promises";
|
|
1
|
+
import { readdir, unlink } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { copyIfMissing, isEnoentError } from "./utils/fs-helpers";
|
|
4
4
|
import { getAssetsDir, getGlobalConfigDir } from "./utils/paths";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Assets that were previously shipped but have since been removed from the source repo.
|
|
8
|
+
* These are cleaned up from the target directory on every install to avoid stale files.
|
|
9
|
+
*/
|
|
10
|
+
const DEPRECATED_ASSETS = ["agents/placeholder-agent.md", "commands/configure.md"] as const;
|
|
11
|
+
|
|
6
12
|
export interface InstallResult {
|
|
7
13
|
readonly copied: readonly string[];
|
|
8
14
|
readonly skipped: readonly string[];
|
|
@@ -112,10 +118,33 @@ async function processSkills(sourceDir: string, targetDir: string): Promise<Inst
|
|
|
112
118
|
return { copied, skipped, errors };
|
|
113
119
|
}
|
|
114
120
|
|
|
121
|
+
async function cleanupDeprecatedAssets(
|
|
122
|
+
targetDir: string,
|
|
123
|
+
): Promise<{ readonly removed: readonly string[]; readonly errors: readonly string[] }> {
|
|
124
|
+
const removed: string[] = [];
|
|
125
|
+
const errors: string[] = [];
|
|
126
|
+
for (const asset of DEPRECATED_ASSETS) {
|
|
127
|
+
try {
|
|
128
|
+
await unlink(join(targetDir, asset));
|
|
129
|
+
removed.push(asset);
|
|
130
|
+
} catch (error: unknown) {
|
|
131
|
+
if (!isEnoentError(error)) {
|
|
132
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
+
errors.push(`cleanup ${asset}: ${message}`);
|
|
134
|
+
}
|
|
135
|
+
// ENOENT is expected — asset already gone
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { removed, errors };
|
|
139
|
+
}
|
|
140
|
+
|
|
115
141
|
export async function installAssets(
|
|
116
142
|
assetsDir: string = getAssetsDir(),
|
|
117
143
|
targetDir: string = getGlobalConfigDir(),
|
|
118
144
|
): Promise<InstallResult> {
|
|
145
|
+
// Remove deprecated assets before copying new ones
|
|
146
|
+
const cleanup = await cleanupDeprecatedAssets(targetDir);
|
|
147
|
+
|
|
119
148
|
const [agents, commands, skills] = await Promise.all([
|
|
120
149
|
processFiles(assetsDir, targetDir, "agents"),
|
|
121
150
|
processFiles(assetsDir, targetDir, "commands"),
|
|
@@ -125,6 +154,6 @@ export async function installAssets(
|
|
|
125
154
|
return {
|
|
126
155
|
copied: [...agents.copied, ...commands.copied, ...skills.copied],
|
|
127
156
|
skipped: [...agents.skipped, ...commands.skipped, ...skills.skipped],
|
|
128
|
-
errors: [...agents.errors, ...commands.errors, ...skills.errors],
|
|
157
|
+
errors: [...cleanup.errors, ...agents.errors, ...commands.errors, ...skills.errors],
|
|
129
158
|
};
|
|
130
159
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { DIVERSITY_RULES } from "./model-groups";
|
|
2
|
+
import { extractFamily } from "./resolver";
|
|
3
|
+
import type { DiversityWarning, GroupId, GroupModelAssignment } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check all diversity rules against current group assignments.
|
|
7
|
+
* Returns warnings for adversarial pairs that share the same model family.
|
|
8
|
+
*
|
|
9
|
+
* Only checks groups that are assigned — unassigned groups are skipped
|
|
10
|
+
* (no warning for missing assignments; that's the config validator's job).
|
|
11
|
+
*/
|
|
12
|
+
export function checkDiversity(
|
|
13
|
+
groups: Readonly<Record<string, GroupModelAssignment>>,
|
|
14
|
+
): readonly DiversityWarning[] {
|
|
15
|
+
const warnings: DiversityWarning[] = [];
|
|
16
|
+
|
|
17
|
+
for (const rule of DIVERSITY_RULES) {
|
|
18
|
+
// Extract families for all assigned groups in this rule
|
|
19
|
+
const families = new Map<GroupId, string>();
|
|
20
|
+
for (const groupId of rule.groups) {
|
|
21
|
+
const assignment = groups[groupId];
|
|
22
|
+
if (assignment) {
|
|
23
|
+
families.set(groupId, extractFamily(assignment.primary));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// All groups in the rule must be assigned to check diversity
|
|
28
|
+
if (families.size < rule.groups.length) continue;
|
|
29
|
+
|
|
30
|
+
// For 2-group rules: warn if both use same family
|
|
31
|
+
// For 3-group rules (red-team + builders + reviewers):
|
|
32
|
+
// warn if red-team shares family with ANY of the others
|
|
33
|
+
if (rule.groups.length === 2) {
|
|
34
|
+
const [familyA, familyB] = [...families.values()];
|
|
35
|
+
if (familyA === familyB) {
|
|
36
|
+
warnings.push({
|
|
37
|
+
rule,
|
|
38
|
+
sharedFamily: familyA,
|
|
39
|
+
groups: [...families.keys()],
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
} else if (rule.groups.includes("red-team" as GroupId)) {
|
|
43
|
+
// 3-group rule with red-team: only compare red-team against the others
|
|
44
|
+
// (builders↔reviewers is already covered by its own 2-group strong rule)
|
|
45
|
+
const redTeamFamily = families.get("red-team" as GroupId);
|
|
46
|
+
if (redTeamFamily) {
|
|
47
|
+
const conflicting = rule.groups.filter(
|
|
48
|
+
(g) => g !== ("red-team" as GroupId) && families.get(g) === redTeamFamily,
|
|
49
|
+
);
|
|
50
|
+
if (conflicting.length > 0) {
|
|
51
|
+
warnings.push({
|
|
52
|
+
rule,
|
|
53
|
+
sharedFamily: redTeamFamily,
|
|
54
|
+
groups: ["red-team" as GroupId, ...conflicting],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
// Generic multi-group rule: check if any pair shares a family
|
|
60
|
+
const entries = [...families.entries()];
|
|
61
|
+
const seen = new Set<string>();
|
|
62
|
+
for (const [, family] of entries) {
|
|
63
|
+
if (seen.has(family)) {
|
|
64
|
+
const sharing = entries.filter(([, f]) => f === family).map(([g]) => g);
|
|
65
|
+
warnings.push({
|
|
66
|
+
rule,
|
|
67
|
+
sharedFamily: family,
|
|
68
|
+
groups: sharing,
|
|
69
|
+
});
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
seen.add(family);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return Object.freeze(warnings);
|
|
78
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { checkDiversity } from "./diversity";
|
|
2
|
+
import { ALL_GROUP_IDS } from "./model-groups";
|
|
3
|
+
import type { DiversityWarning, GroupModelAssignment } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Plugin config shape expected by diagnose().
|
|
7
|
+
* Minimal subset to avoid coupling to the full PluginConfig Zod schema.
|
|
8
|
+
*/
|
|
9
|
+
export interface DiagnosableConfig {
|
|
10
|
+
readonly configured: boolean;
|
|
11
|
+
readonly version: number;
|
|
12
|
+
readonly groups: Readonly<Record<string, GroupModelAssignment>>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Structured result of a diagnostic check, suitable for both
|
|
17
|
+
* CLI terminal formatting and JSON serialization by the tool.
|
|
18
|
+
*/
|
|
19
|
+
export interface DiagnosticResult {
|
|
20
|
+
readonly configExists: boolean;
|
|
21
|
+
readonly schemaValid: boolean;
|
|
22
|
+
readonly configured: boolean;
|
|
23
|
+
readonly groupsAssigned: Readonly<
|
|
24
|
+
Record<
|
|
25
|
+
string,
|
|
26
|
+
{
|
|
27
|
+
readonly assigned: boolean;
|
|
28
|
+
readonly primary: string | null;
|
|
29
|
+
readonly fallbacks: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
>
|
|
32
|
+
>;
|
|
33
|
+
readonly diversityWarnings: readonly DiversityWarning[];
|
|
34
|
+
readonly allPassed: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Pure diagnostic function — inspects a config (or null) and returns
|
|
39
|
+
* a structured result. No side effects, no I/O.
|
|
40
|
+
*
|
|
41
|
+
* Both `bin/cli.ts:runDoctor()` and `src/tools/configure.ts:handleDoctor()`
|
|
42
|
+
* call this and format the result differently (terminal vs JSON).
|
|
43
|
+
*/
|
|
44
|
+
export function diagnose(config: DiagnosableConfig | null): DiagnosticResult {
|
|
45
|
+
const configExists = config !== null;
|
|
46
|
+
// Always true when configExists — loadConfig validates through Zod on load
|
|
47
|
+
const schemaValid = configExists;
|
|
48
|
+
const configured = config?.configured ?? false;
|
|
49
|
+
|
|
50
|
+
const groupsAssigned: Record<
|
|
51
|
+
string,
|
|
52
|
+
{ assigned: boolean; primary: string | null; fallbacks: readonly string[] }
|
|
53
|
+
> = {};
|
|
54
|
+
|
|
55
|
+
for (const groupId of ALL_GROUP_IDS) {
|
|
56
|
+
const assignment = config?.groups[groupId];
|
|
57
|
+
groupsAssigned[groupId] = assignment
|
|
58
|
+
? { assigned: true, primary: assignment.primary, fallbacks: assignment.fallbacks }
|
|
59
|
+
: { assigned: false, primary: null, fallbacks: [] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Diversity check on assigned groups
|
|
63
|
+
const assignedGroups: Record<string, GroupModelAssignment> = {};
|
|
64
|
+
if (config) {
|
|
65
|
+
for (const [key, val] of Object.entries(config.groups)) {
|
|
66
|
+
assignedGroups[key] = val;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const diversityWarnings = checkDiversity(assignedGroups);
|
|
71
|
+
|
|
72
|
+
const allGroupsAssigned = ALL_GROUP_IDS.every((id) => groupsAssigned[id]?.assigned);
|
|
73
|
+
// diversityWarnings are advisory — they don't affect allPassed
|
|
74
|
+
const allPassed = configExists && schemaValid && configured && allGroupsAssigned;
|
|
75
|
+
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
configExists,
|
|
78
|
+
schemaValid,
|
|
79
|
+
configured,
|
|
80
|
+
groupsAssigned: Object.freeze(groupsAssigned),
|
|
81
|
+
diversityWarnings,
|
|
82
|
+
allPassed,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { AgentEntry, DiversityRule, GroupDefinition, GroupId } from "./types";
|
|
2
|
+
|
|
3
|
+
function deepFreeze<T extends object>(obj: T): Readonly<T> {
|
|
4
|
+
for (const value of Object.values(obj)) {
|
|
5
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
6
|
+
deepFreeze(value as object);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return Object.freeze(obj);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const AGENT_REGISTRY: Readonly<Record<string, AgentEntry>> = deepFreeze({
|
|
13
|
+
// ── Architects ─────────────────────────────────────────────
|
|
14
|
+
// Deep reasoning: system design, task decomposition, orchestration
|
|
15
|
+
"oc-architect": { group: "architects" },
|
|
16
|
+
"oc-planner": { group: "architects" },
|
|
17
|
+
autopilot: { group: "architects" },
|
|
18
|
+
|
|
19
|
+
// ── Challengers ────────────────────────────────────────────
|
|
20
|
+
// Adversarial to Architects: critique proposals, enhance ideas
|
|
21
|
+
"oc-critic": { group: "challengers" },
|
|
22
|
+
"oc-challenger": { group: "challengers" },
|
|
23
|
+
|
|
24
|
+
// ── Builders ───────────────────────────────────────────────
|
|
25
|
+
// Code generation
|
|
26
|
+
"oc-implementer": { group: "builders" },
|
|
27
|
+
|
|
28
|
+
// ── Reviewers ──────────────────────────────────────────────
|
|
29
|
+
// Code analysis, adversarial to Builders
|
|
30
|
+
// NOTE: The 21 internal ReviewAgent objects (logic-auditor, security-auditor,
|
|
31
|
+
// etc.) are NOT in this registry. They use the ReviewAgent type from
|
|
32
|
+
// src/review/types.ts, not AgentConfig. The review pipeline resolves their
|
|
33
|
+
// model via resolveModelForGroup("reviewers") directly.
|
|
34
|
+
"oc-reviewer": { group: "reviewers" },
|
|
35
|
+
|
|
36
|
+
// ── Red Team ───────────────────────────────────────────────
|
|
37
|
+
// Final adversarial pass
|
|
38
|
+
// NOTE: red-team and product-thinker are ALSO internal ReviewAgent objects
|
|
39
|
+
// in STAGE3_AGENTS (src/review/agents/index.ts). They appear here so the
|
|
40
|
+
// review pipeline can resolve their model via resolveModelForGroup("red-team")
|
|
41
|
+
// separately from the "reviewers" group.
|
|
42
|
+
"red-team": { group: "red-team" },
|
|
43
|
+
"product-thinker": { group: "red-team" },
|
|
44
|
+
|
|
45
|
+
// ── Researchers ────────────────────────────────────────────
|
|
46
|
+
// Domain research, feasibility analysis
|
|
47
|
+
"oc-researcher": { group: "researchers" },
|
|
48
|
+
researcher: { group: "researchers" },
|
|
49
|
+
|
|
50
|
+
// ── Communicators ──────────────────────────────────────────
|
|
51
|
+
// Docs, changelogs, lesson extraction
|
|
52
|
+
"oc-shipper": { group: "communicators" },
|
|
53
|
+
documenter: { group: "communicators" },
|
|
54
|
+
"oc-retrospector": { group: "communicators" },
|
|
55
|
+
|
|
56
|
+
// ── Utilities ──────────────────────────────────────────────
|
|
57
|
+
// Fast lookups, prompt tuning, PR scanning
|
|
58
|
+
"oc-explorer": { group: "utilities" },
|
|
59
|
+
metaprompter: { group: "utilities" },
|
|
60
|
+
"pr-reviewer": { group: "utilities" },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const GROUP_DEFINITIONS: Readonly<Record<GroupId, GroupDefinition>> = deepFreeze({
|
|
64
|
+
architects: {
|
|
65
|
+
id: "architects",
|
|
66
|
+
label: "Architects",
|
|
67
|
+
purpose: "System design, task decomposition, pipeline orchestration",
|
|
68
|
+
recommendation:
|
|
69
|
+
"Most powerful model available. Bad architecture cascades into everything downstream.",
|
|
70
|
+
tier: "heavy",
|
|
71
|
+
order: 1,
|
|
72
|
+
},
|
|
73
|
+
challengers: {
|
|
74
|
+
id: "challengers",
|
|
75
|
+
label: "Challengers",
|
|
76
|
+
purpose: "Challenge architecture proposals, enhance ideas, find design flaws",
|
|
77
|
+
recommendation:
|
|
78
|
+
"Strong model, different family from Architects for genuine adversarial review.",
|
|
79
|
+
tier: "heavy",
|
|
80
|
+
order: 2,
|
|
81
|
+
},
|
|
82
|
+
builders: {
|
|
83
|
+
id: "builders",
|
|
84
|
+
label: "Builders",
|
|
85
|
+
purpose: "Write production code",
|
|
86
|
+
recommendation: "Strong coding model. This is where most tokens are spent.",
|
|
87
|
+
tier: "heavy",
|
|
88
|
+
order: 3,
|
|
89
|
+
},
|
|
90
|
+
reviewers: {
|
|
91
|
+
id: "reviewers",
|
|
92
|
+
label: "Reviewers",
|
|
93
|
+
purpose: "Find bugs, security issues, logic errors in code",
|
|
94
|
+
recommendation:
|
|
95
|
+
"Strong model, different family from Builders to catch different classes of bugs.",
|
|
96
|
+
tier: "heavy",
|
|
97
|
+
order: 4,
|
|
98
|
+
},
|
|
99
|
+
"red-team": {
|
|
100
|
+
id: "red-team",
|
|
101
|
+
label: "Red Team",
|
|
102
|
+
purpose: "Final adversarial pass — hunt exploits, find UX gaps",
|
|
103
|
+
recommendation: "Different family from both Builders and Reviewers for a third perspective.",
|
|
104
|
+
tier: "heavy",
|
|
105
|
+
order: 5,
|
|
106
|
+
},
|
|
107
|
+
researchers: {
|
|
108
|
+
id: "researchers",
|
|
109
|
+
label: "Researchers",
|
|
110
|
+
purpose: "Domain research, feasibility analysis, information gathering",
|
|
111
|
+
recommendation: "Good context window and comprehension. Any model family works.",
|
|
112
|
+
tier: "medium",
|
|
113
|
+
order: 6,
|
|
114
|
+
},
|
|
115
|
+
communicators: {
|
|
116
|
+
id: "communicators",
|
|
117
|
+
label: "Communicators",
|
|
118
|
+
purpose: "Write docs, changelogs, extract lessons",
|
|
119
|
+
recommendation: "Mid-tier model. Clear writing matters more than deep reasoning.",
|
|
120
|
+
tier: "light",
|
|
121
|
+
order: 7,
|
|
122
|
+
},
|
|
123
|
+
utilities: {
|
|
124
|
+
id: "utilities",
|
|
125
|
+
label: "Utilities",
|
|
126
|
+
purpose: "Fast lookups, prompt tuning, PR scanning",
|
|
127
|
+
recommendation:
|
|
128
|
+
"Fastest available model. Speed over intelligence — don't waste expensive tokens on grep.",
|
|
129
|
+
tier: "light",
|
|
130
|
+
order: 8,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* All valid group IDs, derived from GROUP_DEFINITIONS and sorted by order.
|
|
136
|
+
* Adding a new group to GROUP_DEFINITIONS auto-includes it here.
|
|
137
|
+
*/
|
|
138
|
+
export const ALL_GROUP_IDS: readonly GroupId[] = Object.freeze(
|
|
139
|
+
[...(Object.values(GROUP_DEFINITIONS) as GroupDefinition[])]
|
|
140
|
+
.sort((a: GroupDefinition, b: GroupDefinition) => a.order - b.order)
|
|
141
|
+
.map((d: GroupDefinition) => d.id),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
export const DIVERSITY_RULES: readonly DiversityRule[] = Object.freeze([
|
|
145
|
+
Object.freeze({
|
|
146
|
+
groups: Object.freeze(["architects", "challengers"] as const),
|
|
147
|
+
severity: "strong" as const,
|
|
148
|
+
reason:
|
|
149
|
+
"Challengers critique architect output. Same-model review creates confirmation bias — the model agrees with its own reasoning patterns.",
|
|
150
|
+
}),
|
|
151
|
+
Object.freeze({
|
|
152
|
+
groups: Object.freeze(["builders", "reviewers"] as const),
|
|
153
|
+
severity: "strong" as const,
|
|
154
|
+
reason:
|
|
155
|
+
"Reviewers find bugs in builder code. Same model shares the same blind spots — it won't catch errors it would also make.",
|
|
156
|
+
}),
|
|
157
|
+
Object.freeze({
|
|
158
|
+
groups: Object.freeze(["red-team", "builders", "reviewers"] as const),
|
|
159
|
+
severity: "soft" as const,
|
|
160
|
+
reason:
|
|
161
|
+
"Red Team is most effective as a third perspective. If you only have 2 model families, use whichever isn't assigned to Reviewers.",
|
|
162
|
+
}),
|
|
163
|
+
]);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { AGENT_REGISTRY } from "./model-groups";
|
|
2
|
+
import type { AgentOverride, GroupId, GroupModelAssignment, ResolvedModel } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extract model family (provider) from a model string.
|
|
6
|
+
* "anthropic/claude-opus-4-6" → "anthropic"
|
|
7
|
+
* "openai/gpt-5.4" → "openai"
|
|
8
|
+
* Returns the full string if no "/" is found.
|
|
9
|
+
*/
|
|
10
|
+
export function extractFamily(model: string): string {
|
|
11
|
+
const idx = model.indexOf("/");
|
|
12
|
+
return idx === -1 ? model : model.slice(0, idx);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve model for a named agent.
|
|
17
|
+
*
|
|
18
|
+
* Resolution order:
|
|
19
|
+
* 1. Per-agent override in overrides[agentName]
|
|
20
|
+
* 2. Agent's group in AGENT_REGISTRY → groups[groupId]
|
|
21
|
+
* 3. null (agent uses OpenCode's default model)
|
|
22
|
+
*/
|
|
23
|
+
export function resolveModelForAgent(
|
|
24
|
+
agentName: string,
|
|
25
|
+
groups: Readonly<Record<string, GroupModelAssignment>>,
|
|
26
|
+
overrides: Readonly<Record<string, AgentOverride>>,
|
|
27
|
+
): ResolvedModel | null {
|
|
28
|
+
// Tier 1: per-agent override
|
|
29
|
+
const override = overrides[agentName];
|
|
30
|
+
if (override) {
|
|
31
|
+
return {
|
|
32
|
+
primary: override.primary,
|
|
33
|
+
fallbacks: override.fallbacks ?? [],
|
|
34
|
+
source: "override",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Tier 2: group assignment
|
|
39
|
+
const entry = AGENT_REGISTRY[agentName];
|
|
40
|
+
if (entry) {
|
|
41
|
+
const group = groups[entry.group];
|
|
42
|
+
if (group) {
|
|
43
|
+
return {
|
|
44
|
+
primary: group.primary,
|
|
45
|
+
fallbacks: group.fallbacks,
|
|
46
|
+
source: "group",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Tier 3: no assignment
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve model for a group directly (used by review pipeline for
|
|
57
|
+
* internal ReviewAgent objects that are not in AGENT_REGISTRY).
|
|
58
|
+
*
|
|
59
|
+
* Used by:
|
|
60
|
+
* - Review pipeline for the 19 universal+specialized agents → "reviewers"
|
|
61
|
+
* - Review pipeline for red-team + product-thinker → "red-team"
|
|
62
|
+
*/
|
|
63
|
+
export function resolveModelForGroup(
|
|
64
|
+
groupId: GroupId,
|
|
65
|
+
groups: Readonly<Record<string, GroupModelAssignment>>,
|
|
66
|
+
): ResolvedModel | null {
|
|
67
|
+
const group = groups[groupId];
|
|
68
|
+
if (!group) return null;
|
|
69
|
+
return {
|
|
70
|
+
primary: group.primary,
|
|
71
|
+
fallbacks: group.fallbacks,
|
|
72
|
+
source: "group",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// src/registry/types.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The 8 model groups. Used as keys in config.groups and GROUP_DEFINITIONS.
|
|
5
|
+
*/
|
|
6
|
+
export type GroupId =
|
|
7
|
+
| "architects"
|
|
8
|
+
| "challengers"
|
|
9
|
+
| "builders"
|
|
10
|
+
| "reviewers"
|
|
11
|
+
| "red-team"
|
|
12
|
+
| "researchers"
|
|
13
|
+
| "communicators"
|
|
14
|
+
| "utilities";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Model tier hint — used for display ordering and recommendations.
|
|
18
|
+
* Does not affect resolution logic.
|
|
19
|
+
*/
|
|
20
|
+
export type ModelTier = "heavy" | "medium" | "light";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Diversity warning severity.
|
|
24
|
+
* "strong" — displayed with warning icon, explicitly recommended to change.
|
|
25
|
+
* "soft" — displayed as suggestion, not a strong recommendation.
|
|
26
|
+
*/
|
|
27
|
+
export type DiversitySeverity = "strong" | "soft";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Entry in the agent registry. Maps an agent name to its group.
|
|
31
|
+
*/
|
|
32
|
+
export interface AgentEntry {
|
|
33
|
+
readonly group: GroupId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Metadata for a model group. Pure display/recommendation data.
|
|
38
|
+
*/
|
|
39
|
+
export interface GroupDefinition {
|
|
40
|
+
readonly id: GroupId;
|
|
41
|
+
readonly label: string;
|
|
42
|
+
readonly purpose: string;
|
|
43
|
+
readonly recommendation: string;
|
|
44
|
+
readonly tier: ModelTier;
|
|
45
|
+
readonly order: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Adversarial diversity rule. Declares which groups should use
|
|
50
|
+
* different model families for quality benefits.
|
|
51
|
+
*/
|
|
52
|
+
export interface DiversityRule {
|
|
53
|
+
readonly groups: readonly GroupId[];
|
|
54
|
+
readonly severity: DiversitySeverity;
|
|
55
|
+
readonly reason: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A model assignment for a group (stored in config).
|
|
60
|
+
*/
|
|
61
|
+
export interface GroupModelAssignment {
|
|
62
|
+
readonly primary: string;
|
|
63
|
+
readonly fallbacks: readonly string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A per-agent override (stored in config.overrides).
|
|
68
|
+
*/
|
|
69
|
+
export interface AgentOverride {
|
|
70
|
+
readonly primary: string;
|
|
71
|
+
readonly fallbacks?: readonly string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolved model for an agent — returned by the resolver.
|
|
76
|
+
* `null` means no assignment found; agent uses OpenCode's default.
|
|
77
|
+
*/
|
|
78
|
+
export interface ResolvedModel {
|
|
79
|
+
readonly primary: string;
|
|
80
|
+
readonly fallbacks: readonly string[];
|
|
81
|
+
readonly source: "override" | "group" | "default";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Diversity warning emitted by checkDiversity().
|
|
86
|
+
*/
|
|
87
|
+
export interface DiversityWarning {
|
|
88
|
+
readonly rule: DiversityRule;
|
|
89
|
+
readonly sharedFamily: string;
|
|
90
|
+
readonly groups: readonly GroupId[];
|
|
91
|
+
}
|