opencode-akane 0.1.3 → 0.1.5

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 CHANGED
@@ -73,6 +73,26 @@ For package-based installation, publish this repository to npm and add it to the
73
73
  Akane still reads its runtime config from `~/.config/opencode/akane.json`.
74
74
  If that file does not exist yet, the plugin now bootstraps it automatically on first load with the default config.
75
75
  You only need to edit it when you want to override the default role or artifact settings.
76
+ By default, Akane stays model-first and does not depend on `oh-my-opencode`.
77
+ If you want to opt into OMO agents, set `workflow.preferAgents` to `true`.
78
+
79
+ `akane.json` supports both model routing and agent routing:
80
+
81
+ ```json
82
+ {
83
+ "workflow": {
84
+ "preferAgents": true
85
+ },
86
+ "roleAgents": {
87
+ "planner": "prometheus",
88
+ "plan_reviewer": "hephaestus",
89
+ "implementer": "atlas",
90
+ "reviewer_codex": "momus",
91
+ "reviewer_claude": "oracle",
92
+ "synthesizer": "sisyphus"
93
+ }
94
+ }
95
+ ```
76
96
 
77
97
  ## Publish flow
78
98
 
package/dist/artifacts.js CHANGED
@@ -56,6 +56,7 @@ export function createInitialState(input) {
56
56
  activeStage: null,
57
57
  stageOrder: [...input.config.workflow.stageOrder],
58
58
  stages,
59
+ roleAgents: { ...input.config.roleAgents },
59
60
  roles: { ...input.config.roles },
60
61
  };
61
62
  }
@@ -109,6 +110,21 @@ export async function ensureArtifactLayout(input) {
109
110
  await writeStateFile(statePath, state);
110
111
  createdFiles.push(statePath);
111
112
  }
113
+ else {
114
+ let updated = false;
115
+ if (!state.roleAgents) {
116
+ state.roleAgents = { ...input.config.roleAgents };
117
+ updated = true;
118
+ }
119
+ if (!state.roles) {
120
+ state.roles = { ...input.config.roles };
121
+ updated = true;
122
+ }
123
+ if (updated) {
124
+ state.updatedAt = nowIso();
125
+ await writeStateFile(statePath, state);
126
+ }
127
+ }
112
128
  return { artifactDir, statePath, state, createdFiles };
113
129
  }
114
130
  export async function writeStageArtifact(input) {
package/dist/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { AKANE_SERVICE_NAME, AKANE_STAGE_IDS, DEFAULT_ARTIFACT_DIR, DEFAULT_GLOBAL_CONFIG_PATH, DEFAULT_PLUGIN_OUTPUT_PATH, DEFAULT_ROLE_MODELS, DEFAULT_STAGE_FILES, DEFAULT_STAGE_ORDER, DEFAULT_STATE_FILE, } from "./constants.js";
4
+ import { AKANE_SERVICE_NAME, AKANE_ROLE_IDS, AKANE_STAGE_IDS, DEFAULT_ARTIFACT_DIR, DEFAULT_GLOBAL_CONFIG_PATH, DEFAULT_PLUGIN_OUTPUT_PATH, DEFAULT_ROLE_AGENTS, DEFAULT_ROLE_MODELS, DEFAULT_STAGE_FILES, DEFAULT_STAGE_ORDER, DEFAULT_STATE_FILE, } from "./constants.js";
5
5
  function isRecord(value) {
6
6
  return typeof value === "object" && value !== null && !Array.isArray(value);
7
7
  }
@@ -50,7 +50,9 @@ export function defaultAkaneConfig(configPath = DEFAULT_GLOBAL_CONFIG_PATH) {
50
50
  },
51
51
  workflow: {
52
52
  stageOrder: [...DEFAULT_STAGE_ORDER],
53
+ preferAgents: false,
53
54
  },
55
+ roleAgents: { ...DEFAULT_ROLE_AGENTS },
54
56
  roles: { ...DEFAULT_ROLE_MODELS },
55
57
  };
56
58
  }
@@ -76,6 +78,19 @@ function normalizeRoles(input, fallback) {
76
78
  }
77
79
  return next;
78
80
  }
81
+ function normalizeRoleAgents(input, fallback) {
82
+ if (!isRecord(input)) {
83
+ return { ...fallback };
84
+ }
85
+ const next = { ...fallback };
86
+ for (const role of AKANE_ROLE_IDS) {
87
+ const candidate = input[role];
88
+ if (typeof candidate === "string" && candidate.trim()) {
89
+ next[role] = candidate.trim();
90
+ }
91
+ }
92
+ return next;
93
+ }
79
94
  function normalizeFiles(input, fallback) {
80
95
  if (!isRecord(input)) {
81
96
  return { ...fallback };
@@ -126,7 +141,12 @@ export function mergeAkaneConfig(base, overrides) {
126
141
  },
127
142
  workflow: {
128
143
  stageOrder: normalizeStageOrder(isRecord(overrides.workflow) ? overrides.workflow.stageOrder : undefined, base.workflow.stageOrder),
144
+ preferAgents: isRecord(overrides.workflow) &&
145
+ typeof overrides.workflow.preferAgents === "boolean"
146
+ ? overrides.workflow.preferAgents
147
+ : base.workflow.preferAgents,
129
148
  },
149
+ roleAgents: normalizeRoleAgents(overrides.roleAgents, base.roleAgents),
130
150
  roles: normalizeRoles(overrides.roles, base.roles),
131
151
  };
132
152
  }
@@ -16,6 +16,16 @@ export declare const DEFAULT_ROLE_MODELS: {
16
16
  readonly reviewer_claude: "anthropic/claude-opus-4-6";
17
17
  readonly synthesizer: "openai/gpt-5.4";
18
18
  };
19
+ export declare const DEFAULT_ROLE_AGENTS: {
20
+ readonly planner: "prometheus";
21
+ readonly plan_reviewer: "metis";
22
+ readonly implementer: "atlas";
23
+ readonly consultant_primary: "oracle";
24
+ readonly consultant_secondary: "librarian";
25
+ readonly reviewer_codex: "momus";
26
+ readonly reviewer_claude: "oracle";
27
+ readonly synthesizer: "sisyphus";
28
+ };
19
29
  export declare const DEFAULT_STAGE_FILES: {
20
30
  readonly plan: "plan.md";
21
31
  readonly "plan-review": "plan-review.md";
package/dist/constants.js CHANGED
@@ -41,6 +41,16 @@ export const DEFAULT_ROLE_MODELS = {
41
41
  reviewer_claude: "anthropic/claude-opus-4-6",
42
42
  synthesizer: "openai/gpt-5.4",
43
43
  };
44
+ export const DEFAULT_ROLE_AGENTS = {
45
+ planner: "prometheus",
46
+ plan_reviewer: "metis",
47
+ implementer: "atlas",
48
+ consultant_primary: "oracle",
49
+ consultant_secondary: "librarian",
50
+ reviewer_codex: "momus",
51
+ reviewer_claude: "oracle",
52
+ synthesizer: "sisyphus",
53
+ };
44
54
  export const DEFAULT_STAGE_FILES = {
45
55
  plan: "plan.md",
46
56
  "plan-review": "plan-review.md",
package/dist/types.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { AKANE_ROLE_IDS, AKANE_STAGE_IDS, DEFAULT_ROLE_MODELS, DEFAULT_STAG
2
2
  export type AkaneStageId = (typeof AKANE_STAGE_IDS)[number];
3
3
  export type AkaneRoleId = (typeof AKANE_ROLE_IDS)[number];
4
4
  export type AkaneRoles = Record<AkaneRoleId, string>;
5
+ export type AkaneRoleAgents = Record<AkaneRoleId, string>;
5
6
  export type AkaneStageFiles = Record<AkaneStageId, string>;
6
7
  export interface AkaneConfig {
7
8
  version: number;
@@ -15,7 +16,9 @@ export interface AkaneConfig {
15
16
  };
16
17
  workflow: {
17
18
  stageOrder: AkaneStageId[];
19
+ preferAgents: boolean;
18
20
  };
21
+ roleAgents: AkaneRoleAgents;
19
22
  roles: AkaneRoles;
20
23
  }
21
24
  export interface LoadedAkaneConfig {
@@ -28,6 +31,7 @@ export interface AkaneStageState {
28
31
  status: "pending" | "initialized" | "completed";
29
32
  updatedAt: string | null;
30
33
  role?: AkaneRoleId;
34
+ agent?: string;
31
35
  model?: string;
32
36
  sessionID?: string;
33
37
  messageID?: string;
@@ -44,6 +48,7 @@ export interface AkaneState {
44
48
  activeStage: AkaneStageId | null;
45
49
  stageOrder: AkaneStageId[];
46
50
  stages: Record<AkaneStageId, AkaneStageState>;
51
+ roleAgents: AkaneRoleAgents;
47
52
  roles: AkaneRoles;
48
53
  }
49
54
  export type ArtifactWriteMode = "append" | "replace";
package/dist/workflow.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { execFile } from "node:child_process";
3
3
  import { promisify } from "node:util";
4
- import { AKANE_SERVICE_NAME, AKANE_TOOL_IDS, } from "./constants.js";
4
+ import { AKANE_SERVICE_NAME, AKANE_TOOL_IDS, DEFAULT_ROLE_AGENTS, } from "./constants.js";
5
5
  import { ensureArtifactLayout, resolveProjectRoot, resolveStageArtifactPath, writeStageArtifact, } from "./artifacts.js";
6
6
  const execFileAsync = promisify(execFile);
7
7
  const STAGE_ROLE_MAP = {
@@ -12,13 +12,27 @@ const STAGE_ROLE_MAP = {
12
12
  "review-claude": "reviewer_claude",
13
13
  "final-synthesis": "synthesizer",
14
14
  };
15
- const DEFAULT_ROLE_AGENTS = {
16
- planner: "plan",
17
- plan_reviewer: "general",
18
- implementer: "build",
19
- reviewer_codex: "general",
20
- reviewer_claude: "general",
21
- synthesizer: "general",
15
+ const DEFAULT_ROLE_AGENT_CANDIDATES = {
16
+ planner: ["prometheus", "Prometheus (Plan Builder)", "plan"],
17
+ plan_reviewer: [
18
+ "metis",
19
+ "Metis (Plan Consultant)",
20
+ "hephaestus",
21
+ "Hephaestus (Deep Agent)",
22
+ "general",
23
+ ],
24
+ implementer: ["atlas", "Atlas (Plan Executor)", "build"],
25
+ consultant_primary: ["oracle", "general"],
26
+ consultant_secondary: ["librarian", "explore", "general"],
27
+ reviewer_codex: [
28
+ "momus",
29
+ "Momus (Plan Critic)",
30
+ "hephaestus",
31
+ "Hephaestus (Deep Agent)",
32
+ "general",
33
+ ],
34
+ reviewer_claude: ["oracle", "general"],
35
+ synthesizer: ["sisyphus", "Sisyphus (Ultraworker)", "general"],
22
36
  };
23
37
  const IMPLEMENT_TIMEOUT_MS = 15 * 60 * 1000;
24
38
  const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
@@ -39,6 +53,14 @@ function stageTitle(stage) {
39
53
  .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
40
54
  .join(" ");
41
55
  }
56
+ function normalizeAgentName(input) {
57
+ return input.trim().toLowerCase();
58
+ }
59
+ function agentNameAliases(input) {
60
+ const normalized = normalizeAgentName(input);
61
+ const base = normalized.replace(/\s*\([^)]*\)\s*$/, "").trim();
62
+ return Array.from(new Set([normalized, base].filter(Boolean)));
63
+ }
42
64
  function parseModelRef(modelRef) {
43
65
  const [providerID, ...rest] = modelRef.split("/");
44
66
  const modelID = rest.join("/").trim();
@@ -86,23 +108,36 @@ function findLatestAssistantMessage(messages) {
86
108
  function makeToolRestrictions(allowWorkspaceMutation) {
87
109
  const restrictions = Object.fromEntries(AKANE_TOOL_IDS.map((toolID) => [toolID, false]));
88
110
  if (!allowWorkspaceMutation) {
89
- for (const toolID of ["bash", "edit", "patch", "write", "task", "question"]) {
111
+ for (const toolID of ["bash", "edit", "patch", "write"]) {
90
112
  restrictions[toolID] = false;
91
113
  }
92
114
  }
93
115
  return restrictions;
94
116
  }
95
- async function resolveAgentName(client, directory, role) {
96
- const preferred = DEFAULT_ROLE_AGENTS[role];
97
- if (!preferred) {
117
+ async function resolveAgentName(client, directory, role, config) {
118
+ if (!config.workflow.preferAgents) {
98
119
  return undefined;
99
120
  }
121
+ const candidates = [
122
+ config.roleAgents[role],
123
+ ...DEFAULT_ROLE_AGENT_CANDIDATES[role],
124
+ DEFAULT_ROLE_AGENTS[role],
125
+ ]
126
+ .filter((candidate) => typeof candidate === "string" && candidate.trim().length > 0)
127
+ .filter((candidate, index, all) => all.findIndex((value) => normalizeAgentName(value) === normalizeAgentName(candidate)) === index);
100
128
  try {
101
129
  const result = await client.app.agents({
102
130
  query: { directory },
103
131
  });
104
132
  const agents = requireResultData(result, "agent list");
105
- return agents.some((agent) => agent.name === preferred) ? preferred : undefined;
133
+ for (const candidate of candidates) {
134
+ const candidateAliases = agentNameAliases(candidate);
135
+ const matched = agents.find((agent) => agentNameAliases(agent.name).some((alias) => candidateAliases.includes(alias)));
136
+ if (matched) {
137
+ return matched.name;
138
+ }
139
+ }
140
+ return undefined;
106
141
  }
107
142
  catch {
108
143
  return undefined;
@@ -199,6 +234,7 @@ function renderStageDocument(input) {
199
234
  "",
200
235
  `- Service: ${AKANE_SERVICE_NAME}`,
201
236
  `- Role: ${input.role}`,
237
+ ...(input.agent ? [`- Agent: ${input.agent}`] : []),
202
238
  `- Model: ${input.model}`,
203
239
  `- Session ID: ${input.sessionID}`,
204
240
  `- Message ID: ${input.messageID}`,
@@ -246,9 +282,9 @@ function buildArtifactBlock(title, content) {
246
282
  }
247
283
  async function runStageSession(input) {
248
284
  const role = STAGE_ROLE_MAP[input.stage];
249
- const model = input.configInfo.config.roles[role];
250
- const modelRef = parseModelRef(model);
251
- const agent = await resolveAgentName(input.pluginInput.client, input.projectRoot, role);
285
+ const configuredModel = input.configInfo.config.roles[role];
286
+ const modelRef = parseModelRef(configuredModel);
287
+ const agent = await resolveAgentName(input.pluginInput.client, input.projectRoot, role, input.configInfo.config);
252
288
  const session = await createStageSession({
253
289
  client: input.pluginInput.client,
254
290
  parentSessionID: input.toolContext.sessionID,
@@ -261,7 +297,7 @@ async function runStageSession(input) {
261
297
  signal: input.toolContext.abort,
262
298
  body: {
263
299
  ...(agent ? { agent } : {}),
264
- model: modelRef,
300
+ ...(agent ? {} : { model: modelRef }),
265
301
  system: input.system,
266
302
  tools: makeToolRestrictions(input.allowWorkspaceMutation),
267
303
  parts: [
@@ -293,7 +329,9 @@ async function runStageSession(input) {
293
329
  return {
294
330
  stage: input.stage,
295
331
  role,
296
- model,
332
+ model: latestAssistant.info.providerID && latestAssistant.info.modelID
333
+ ? `${latestAssistant.info.providerID}/${latestAssistant.info.modelID}`
334
+ : configuredModel,
297
335
  agent,
298
336
  sessionID: session.id,
299
337
  messageID: latestAssistant.info.id,
@@ -381,6 +419,7 @@ export async function executePlanStage(input) {
381
419
  const content = renderStageDocument({
382
420
  stage: result.stage,
383
421
  role: result.role,
422
+ agent: result.agent,
384
423
  model: result.model,
385
424
  sessionID: result.sessionID,
386
425
  messageID: result.messageID,
@@ -396,6 +435,7 @@ export async function executePlanStage(input) {
396
435
  mode: "replace",
397
436
  details: {
398
437
  role: result.role,
438
+ agent: result.agent,
399
439
  model: result.model,
400
440
  sessionID: result.sessionID,
401
441
  messageID: result.messageID,
@@ -429,6 +469,7 @@ export async function executePlanReviewStage(input) {
429
469
  const content = renderStageDocument({
430
470
  stage: result.stage,
431
471
  role: result.role,
472
+ agent: result.agent,
432
473
  model: result.model,
433
474
  sessionID: result.sessionID,
434
475
  messageID: result.messageID,
@@ -444,6 +485,7 @@ export async function executePlanReviewStage(input) {
444
485
  mode: "replace",
445
486
  details: {
446
487
  role: result.role,
488
+ agent: result.agent,
447
489
  model: result.model,
448
490
  sessionID: result.sessionID,
449
491
  messageID: result.messageID,
@@ -500,6 +542,7 @@ export async function executeImplementStage(input) {
500
542
  const content = renderStageDocument({
501
543
  stage: result.stage,
502
544
  role: result.role,
545
+ agent: result.agent,
503
546
  model: result.model,
504
547
  sessionID: result.sessionID,
505
548
  messageID: result.messageID,
@@ -530,6 +573,7 @@ export async function executeImplementStage(input) {
530
573
  mode: "replace",
531
574
  details: {
532
575
  role: result.role,
576
+ agent: result.agent,
533
577
  model: result.model,
534
578
  sessionID: result.sessionID,
535
579
  messageID: result.messageID,
@@ -577,6 +621,7 @@ async function executeReviewerStage(input) {
577
621
  const content = renderStageDocument({
578
622
  stage: result.stage,
579
623
  role: result.role,
624
+ agent: result.agent,
580
625
  model: result.model,
581
626
  sessionID: result.sessionID,
582
627
  messageID: result.messageID,
@@ -592,6 +637,7 @@ async function executeReviewerStage(input) {
592
637
  mode: "replace",
593
638
  details: {
594
639
  role: result.role,
640
+ agent: result.agent,
595
641
  model: result.model,
596
642
  sessionID: result.sessionID,
597
643
  messageID: result.messageID,
@@ -655,6 +701,7 @@ export async function executeSynthesizeStage(input) {
655
701
  const content = renderStageDocument({
656
702
  stage: result.stage,
657
703
  role: result.role,
704
+ agent: result.agent,
658
705
  model: result.model,
659
706
  sessionID: result.sessionID,
660
707
  messageID: result.messageID,
@@ -670,6 +717,7 @@ export async function executeSynthesizeStage(input) {
670
717
  mode: "replace",
671
718
  details: {
672
719
  role: result.role,
720
+ agent: result.agent,
673
721
  model: result.model,
674
722
  sessionID: result.sessionID,
675
723
  messageID: result.messageID,
@@ -16,6 +16,7 @@
16
16
  }
17
17
  },
18
18
  "workflow": {
19
+ "preferAgents": false,
19
20
  "stageOrder": [
20
21
  "plan",
21
22
  "plan-review",
@@ -25,6 +26,16 @@
25
26
  "final-synthesis"
26
27
  ]
27
28
  },
29
+ "roleAgents": {
30
+ "planner": "prometheus",
31
+ "plan_reviewer": "metis",
32
+ "implementer": "atlas",
33
+ "consultant_primary": "oracle",
34
+ "consultant_secondary": "librarian",
35
+ "reviewer_codex": "momus",
36
+ "reviewer_claude": "oracle",
37
+ "synthesizer": "sisyphus"
38
+ },
28
39
  "roles": {
29
40
  "planner": "anthropic/claude-opus-4-6",
30
41
  "plan_reviewer": "openai/gpt-5.4",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-akane",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Akane orchestration plugin for OpenCode",
5
5
  "license": "MIT",
6
6
  "type": "module",