opencode-akane 0.1.3 → 0.1.4

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,22 @@ 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
+ When `oh-my-opencode` is installed, Akane now prefers OMO agents such as `prometheus`, `atlas`, `momus`, `oracle`, and `sisyphus`, then falls back to direct model routing when those agents are unavailable.
77
+
78
+ `akane.json` supports both model routing and agent routing:
79
+
80
+ ```json
81
+ {
82
+ "roleAgents": {
83
+ "planner": "prometheus",
84
+ "plan_reviewer": "hephaestus",
85
+ "implementer": "atlas",
86
+ "reviewer_codex": "momus",
87
+ "reviewer_claude": "oracle",
88
+ "synthesizer": "sisyphus"
89
+ }
90
+ }
91
+ ```
76
92
 
77
93
  ## Publish flow
78
94
 
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
  }
@@ -51,6 +51,7 @@ export function defaultAkaneConfig(configPath = DEFAULT_GLOBAL_CONFIG_PATH) {
51
51
  workflow: {
52
52
  stageOrder: [...DEFAULT_STAGE_ORDER],
53
53
  },
54
+ roleAgents: { ...DEFAULT_ROLE_AGENTS },
54
55
  roles: { ...DEFAULT_ROLE_MODELS },
55
56
  };
56
57
  }
@@ -76,6 +77,19 @@ function normalizeRoles(input, fallback) {
76
77
  }
77
78
  return next;
78
79
  }
80
+ function normalizeRoleAgents(input, fallback) {
81
+ if (!isRecord(input)) {
82
+ return { ...fallback };
83
+ }
84
+ const next = { ...fallback };
85
+ for (const role of AKANE_ROLE_IDS) {
86
+ const candidate = input[role];
87
+ if (typeof candidate === "string" && candidate.trim()) {
88
+ next[role] = candidate.trim();
89
+ }
90
+ }
91
+ return next;
92
+ }
79
93
  function normalizeFiles(input, fallback) {
80
94
  if (!isRecord(input)) {
81
95
  return { ...fallback };
@@ -127,6 +141,7 @@ export function mergeAkaneConfig(base, overrides) {
127
141
  workflow: {
128
142
  stageOrder: normalizeStageOrder(isRecord(overrides.workflow) ? overrides.workflow.stageOrder : undefined, base.workflow.stageOrder),
129
143
  },
144
+ roleAgents: normalizeRoleAgents(overrides.roleAgents, base.roleAgents),
130
145
  roles: normalizeRoles(overrides.roles, base.roles),
131
146
  };
132
147
  }
@@ -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;
@@ -16,6 +17,7 @@ export interface AkaneConfig {
16
17
  workflow: {
17
18
  stageOrder: AkaneStageId[];
18
19
  };
20
+ roleAgents: AkaneRoleAgents;
19
21
  roles: AkaneRoles;
20
22
  }
21
23
  export interface LoadedAkaneConfig {
@@ -28,6 +30,7 @@ export interface AkaneStageState {
28
30
  status: "pending" | "initialized" | "completed";
29
31
  updatedAt: string | null;
30
32
  role?: AkaneRoleId;
33
+ agent?: string;
31
34
  model?: string;
32
35
  sessionID?: string;
33
36
  messageID?: string;
@@ -44,6 +47,7 @@ export interface AkaneState {
44
47
  activeStage: AkaneStageId | null;
45
48
  stageOrder: AkaneStageId[];
46
49
  stages: Record<AkaneStageId, AkaneStageState>;
50
+ roleAgents: AkaneRoleAgents;
47
51
  roles: AkaneRoles;
48
52
  }
49
53
  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,33 @@ 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) {
98
- return undefined;
99
- }
117
+ async function resolveAgentName(client, directory, role, config) {
118
+ const candidates = [
119
+ config.roleAgents[role],
120
+ ...DEFAULT_ROLE_AGENT_CANDIDATES[role],
121
+ DEFAULT_ROLE_AGENTS[role],
122
+ ]
123
+ .filter((candidate) => typeof candidate === "string" && candidate.trim().length > 0)
124
+ .filter((candidate, index, all) => all.findIndex((value) => normalizeAgentName(value) === normalizeAgentName(candidate)) === index);
100
125
  try {
101
126
  const result = await client.app.agents({
102
127
  query: { directory },
103
128
  });
104
129
  const agents = requireResultData(result, "agent list");
105
- return agents.some((agent) => agent.name === preferred) ? preferred : undefined;
130
+ for (const candidate of candidates) {
131
+ const candidateAliases = agentNameAliases(candidate);
132
+ const matched = agents.find((agent) => agentNameAliases(agent.name).some((alias) => candidateAliases.includes(alias)));
133
+ if (matched) {
134
+ return matched.name;
135
+ }
136
+ }
137
+ return undefined;
106
138
  }
107
139
  catch {
108
140
  return undefined;
@@ -199,6 +231,7 @@ function renderStageDocument(input) {
199
231
  "",
200
232
  `- Service: ${AKANE_SERVICE_NAME}`,
201
233
  `- Role: ${input.role}`,
234
+ ...(input.agent ? [`- Agent: ${input.agent}`] : []),
202
235
  `- Model: ${input.model}`,
203
236
  `- Session ID: ${input.sessionID}`,
204
237
  `- Message ID: ${input.messageID}`,
@@ -246,9 +279,9 @@ function buildArtifactBlock(title, content) {
246
279
  }
247
280
  async function runStageSession(input) {
248
281
  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);
282
+ const configuredModel = input.configInfo.config.roles[role];
283
+ const modelRef = parseModelRef(configuredModel);
284
+ const agent = await resolveAgentName(input.pluginInput.client, input.projectRoot, role, input.configInfo.config);
252
285
  const session = await createStageSession({
253
286
  client: input.pluginInput.client,
254
287
  parentSessionID: input.toolContext.sessionID,
@@ -261,7 +294,7 @@ async function runStageSession(input) {
261
294
  signal: input.toolContext.abort,
262
295
  body: {
263
296
  ...(agent ? { agent } : {}),
264
- model: modelRef,
297
+ ...(agent ? {} : { model: modelRef }),
265
298
  system: input.system,
266
299
  tools: makeToolRestrictions(input.allowWorkspaceMutation),
267
300
  parts: [
@@ -293,7 +326,9 @@ async function runStageSession(input) {
293
326
  return {
294
327
  stage: input.stage,
295
328
  role,
296
- model,
329
+ model: latestAssistant.info.providerID && latestAssistant.info.modelID
330
+ ? `${latestAssistant.info.providerID}/${latestAssistant.info.modelID}`
331
+ : configuredModel,
297
332
  agent,
298
333
  sessionID: session.id,
299
334
  messageID: latestAssistant.info.id,
@@ -381,6 +416,7 @@ export async function executePlanStage(input) {
381
416
  const content = renderStageDocument({
382
417
  stage: result.stage,
383
418
  role: result.role,
419
+ agent: result.agent,
384
420
  model: result.model,
385
421
  sessionID: result.sessionID,
386
422
  messageID: result.messageID,
@@ -396,6 +432,7 @@ export async function executePlanStage(input) {
396
432
  mode: "replace",
397
433
  details: {
398
434
  role: result.role,
435
+ agent: result.agent,
399
436
  model: result.model,
400
437
  sessionID: result.sessionID,
401
438
  messageID: result.messageID,
@@ -429,6 +466,7 @@ export async function executePlanReviewStage(input) {
429
466
  const content = renderStageDocument({
430
467
  stage: result.stage,
431
468
  role: result.role,
469
+ agent: result.agent,
432
470
  model: result.model,
433
471
  sessionID: result.sessionID,
434
472
  messageID: result.messageID,
@@ -444,6 +482,7 @@ export async function executePlanReviewStage(input) {
444
482
  mode: "replace",
445
483
  details: {
446
484
  role: result.role,
485
+ agent: result.agent,
447
486
  model: result.model,
448
487
  sessionID: result.sessionID,
449
488
  messageID: result.messageID,
@@ -500,6 +539,7 @@ export async function executeImplementStage(input) {
500
539
  const content = renderStageDocument({
501
540
  stage: result.stage,
502
541
  role: result.role,
542
+ agent: result.agent,
503
543
  model: result.model,
504
544
  sessionID: result.sessionID,
505
545
  messageID: result.messageID,
@@ -530,6 +570,7 @@ export async function executeImplementStage(input) {
530
570
  mode: "replace",
531
571
  details: {
532
572
  role: result.role,
573
+ agent: result.agent,
533
574
  model: result.model,
534
575
  sessionID: result.sessionID,
535
576
  messageID: result.messageID,
@@ -577,6 +618,7 @@ async function executeReviewerStage(input) {
577
618
  const content = renderStageDocument({
578
619
  stage: result.stage,
579
620
  role: result.role,
621
+ agent: result.agent,
580
622
  model: result.model,
581
623
  sessionID: result.sessionID,
582
624
  messageID: result.messageID,
@@ -592,6 +634,7 @@ async function executeReviewerStage(input) {
592
634
  mode: "replace",
593
635
  details: {
594
636
  role: result.role,
637
+ agent: result.agent,
595
638
  model: result.model,
596
639
  sessionID: result.sessionID,
597
640
  messageID: result.messageID,
@@ -655,6 +698,7 @@ export async function executeSynthesizeStage(input) {
655
698
  const content = renderStageDocument({
656
699
  stage: result.stage,
657
700
  role: result.role,
701
+ agent: result.agent,
658
702
  model: result.model,
659
703
  sessionID: result.sessionID,
660
704
  messageID: result.messageID,
@@ -670,6 +714,7 @@ export async function executeSynthesizeStage(input) {
670
714
  mode: "replace",
671
715
  details: {
672
716
  role: result.role,
717
+ agent: result.agent,
673
718
  model: result.model,
674
719
  sessionID: result.sessionID,
675
720
  messageID: result.messageID,
@@ -25,6 +25,16 @@
25
25
  "final-synthesis"
26
26
  ]
27
27
  },
28
+ "roleAgents": {
29
+ "planner": "prometheus",
30
+ "plan_reviewer": "metis",
31
+ "implementer": "atlas",
32
+ "consultant_primary": "oracle",
33
+ "consultant_secondary": "librarian",
34
+ "reviewer_codex": "momus",
35
+ "reviewer_claude": "oracle",
36
+ "synthesizer": "sisyphus"
37
+ },
28
38
  "roles": {
29
39
  "planner": "anthropic/claude-opus-4-6",
30
40
  "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.4",
4
4
  "description": "Akane orchestration plugin for OpenCode",
5
5
  "license": "MIT",
6
6
  "type": "module",