@kodrunhq/opencode-autopilot 0.1.3 → 1.0.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/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, 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";
@@ -77,7 +77,9 @@ const plugin: Plugin = async (input) => {
77
77
  const agentConfigs = openCodeConfig?.agent as
78
78
  | Record<string, Record<string, unknown>>
79
79
  | undefined;
80
- return resolveChain(agentName ?? "", agentConfigs, config?.fallback_models);
80
+ // Per-agent fallback_models are populated by configHook from group/override config.
81
+ // resolveChain reads config.agent[agentName].fallback_models (tier 1).
82
+ return resolveChain(agentName ?? "", agentConfigs, undefined);
81
83
  },
82
84
  });
83
85
 
@@ -91,7 +93,7 @@ const plugin: Plugin = async (input) => {
91
93
 
92
94
  return {
93
95
  tool: {
94
- oc_placeholder: ocPlaceholder,
96
+ oc_configure: ocConfigure,
95
97
  oc_create_agent: ocCreateAgent,
96
98
  oc_create_skill: ocCreateSkill,
97
99
  oc_create_command: ocCreateCommand,
@@ -105,8 +107,11 @@ const plugin: Plugin = async (input) => {
105
107
  },
106
108
  event: async ({ event }) => {
107
109
  if (event.type === "session.created" && isFirstLoad(config)) {
108
- // First load: config wizard will be triggered via /configure command
109
- // Phase 2 will add the oc_configure tool
110
+ await sdkOps.showToast(
111
+ "Welcome to OpenCode Autopilot!",
112
+ "Run /oc-configure to set up your model assignments for each agent group.",
113
+ "info",
114
+ );
110
115
  }
111
116
 
112
117
  // Fallback event handling (runs for all events)
@@ -116,6 +121,7 @@ const plugin: Plugin = async (input) => {
116
121
  },
117
122
  config: async (cfg: Config) => {
118
123
  openCodeConfig = cfg;
124
+ setOpenCodeConfig(cfg);
119
125
  await configHook(cfg);
120
126
  },
121
127
  "chat.message": async (
@@ -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
+ }