opencode-akane 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NaturaAurum
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # Akane Agents
2
+
3
+ Akane is a global OpenCode orchestration plugin draft.
4
+
5
+ Current direction:
6
+
7
+ - Keep OpenCode provider and auth infrastructure
8
+ - Keep `oh-my-opencode` installed but neutralized
9
+ - Drive a deterministic workflow across planning, review, implementation, and synthesis
10
+
11
+ See [docs/session-context-20260306.md](/Users/taewoo.kim/Desktop/Projects/akane-agents/docs/session-context-20260306.md) for the current working context.
12
+
13
+ ## MVP scaffold
14
+
15
+ This repository now contains the first MVP skeleton for the plugin:
16
+
17
+ - `src/plugin.ts`: OpenCode plugin entry
18
+ - `src/config.ts`: `~/.config/opencode/akane.json` loader with defaults
19
+ - `src/artifacts.ts`: `.opencode/akane/` artifact and `state.json` helpers
20
+ - `src/tools/akane-init.ts`: initializes the per-project workspace
21
+ - `src/tools/akane-stage-artifact.ts`: writes stage artifacts deterministically
22
+ - `examples/akane.example.json`: example global Akane config
23
+
24
+ ## Local development
25
+
26
+ ```bash
27
+ bun install
28
+ bun run typecheck
29
+ bun run build
30
+ ```
31
+
32
+ The build emits `dist/index.js` as the package entrypoint and also keeps `dist/akane.js` for local file-based linking.
33
+
34
+ ## Package install
35
+
36
+ For package-based installation, publish this repository to npm and add it to the OpenCode plugin array in `~/.config/opencode/opencode.json`:
37
+
38
+ ```json
39
+ {
40
+ "$schema": "https://opencode.ai/config.json",
41
+ "plugin": [
42
+ "oh-my-opencode@latest",
43
+ "opencode-akane@0.1.0"
44
+ ]
45
+ }
46
+ ```
47
+
48
+ Akane still reads its runtime config from `~/.config/opencode/akane.json`.
49
+
50
+ ## Publish flow
51
+
52
+ The package is prepared to publish from npm with:
53
+
54
+ ```bash
55
+ bun install
56
+ bun run typecheck
57
+ bun run build
58
+ npm publish
59
+ ```
60
+
61
+ Useful checks before publishing:
62
+
63
+ ```bash
64
+ bun run pack:check
65
+ ```
66
+
67
+ Notes:
68
+
69
+ - The current package name assumption is `opencode-akane`
70
+ - The current license is `MIT`
71
+ - If you later switch to a scoped package name, publish with `npm publish --access public`
72
+
73
+ ## Automated publishing
74
+
75
+ This repository includes GitHub Actions workflows for CI and npm publishing:
76
+
77
+ - `.github/workflows/ci.yml`: runs install, typecheck, build, and `npm pack --dry-run`
78
+ - `.github/workflows/publish.yml`: publishes to npm when a tag like `v0.1.0` is pushed
79
+
80
+ Recommended setup for public release:
81
+
82
+ 1. Make the GitHub repository public
83
+ 2. Publish `opencode-akane` once manually from your npm account, or reserve the package name
84
+ 3. In npm package settings, add a Trusted Publisher for:
85
+ - GitHub owner: `NaturaAurum`
86
+ - Repository: `akane-agents`
87
+ - Workflow filename: `publish.yml`
88
+ 4. Push a version tag that matches `package.json`, for example:
89
+
90
+ ```bash
91
+ git tag v0.1.0
92
+ git push origin main --tags
93
+ ```
94
+
95
+ The publish workflow intentionally uses npm trusted publishing with GitHub OIDC instead of an `NPM_TOKEN`.
@@ -0,0 +1 @@
1
+ export { AkanePlugin as default, AkanePlugin } from "./plugin.js";
package/dist/akane.js ADDED
@@ -0,0 +1 @@
1
+ export { AkanePlugin as default, AkanePlugin } from "./plugin.js";
@@ -0,0 +1,38 @@
1
+ import type { AkaneConfig, AkaneStageId, AkaneState, ArtifactWriteMode } from "./types.js";
2
+ export declare function resolveProjectRoot(input: {
3
+ directory: string;
4
+ worktree?: string;
5
+ projectRoot?: string;
6
+ }): string;
7
+ export declare function resolveArtifactDir(projectRoot: string, config: AkaneConfig): string;
8
+ export declare function resolveStatePath(projectRoot: string, config: AkaneConfig): string;
9
+ export declare function resolveStageArtifactPath(projectRoot: string, config: AkaneConfig, stage: AkaneStageId): string;
10
+ export declare function createInitialState(input: {
11
+ config: AkaneConfig;
12
+ configPath: string;
13
+ projectRoot: string;
14
+ }): AkaneState;
15
+ export declare function ensureArtifactLayout(input: {
16
+ projectRoot: string;
17
+ config: AkaneConfig;
18
+ configPath: string;
19
+ force?: boolean;
20
+ }): Promise<{
21
+ artifactDir: string;
22
+ statePath: string;
23
+ state: AkaneState;
24
+ createdFiles: string[];
25
+ }>;
26
+ export declare function writeStageArtifact(input: {
27
+ projectRoot: string;
28
+ config: AkaneConfig;
29
+ configPath: string;
30
+ stage: AkaneStageId;
31
+ content: string;
32
+ mode: ArtifactWriteMode;
33
+ }): Promise<{
34
+ artifactDir: string;
35
+ artifactPath: string;
36
+ statePath: string;
37
+ state: AkaneState;
38
+ }>;
@@ -0,0 +1,144 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { AKANE_SERVICE_NAME, AKANE_STAGE_IDS, } from "./constants.js";
4
+ function nowIso() {
5
+ return new Date().toISOString();
6
+ }
7
+ function stageTitle(stage) {
8
+ return stage
9
+ .split("-")
10
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
11
+ .join(" ");
12
+ }
13
+ function defaultStageTemplate(stage) {
14
+ return `# ${stageTitle(stage)}\n\nGenerated by ${AKANE_SERVICE_NAME}.\n`;
15
+ }
16
+ export function resolveProjectRoot(input) {
17
+ if (input.projectRoot && input.projectRoot.trim()) {
18
+ return path.resolve(input.projectRoot);
19
+ }
20
+ if (input.worktree && input.worktree.trim()) {
21
+ return path.resolve(input.worktree);
22
+ }
23
+ return path.resolve(input.directory);
24
+ }
25
+ export function resolveArtifactDir(projectRoot, config) {
26
+ if (path.isAbsolute(config.artifacts.dir)) {
27
+ return config.artifacts.dir;
28
+ }
29
+ return path.join(projectRoot, config.artifacts.dir);
30
+ }
31
+ export function resolveStatePath(projectRoot, config) {
32
+ return path.join(resolveArtifactDir(projectRoot, config), config.artifacts.stateFile);
33
+ }
34
+ export function resolveStageArtifactPath(projectRoot, config, stage) {
35
+ return path.join(resolveArtifactDir(projectRoot, config), config.artifacts.files[stage]);
36
+ }
37
+ export function createInitialState(input) {
38
+ const artifactDir = resolveArtifactDir(input.projectRoot, input.config);
39
+ const timestamp = nowIso();
40
+ const stages = Object.fromEntries(AKANE_STAGE_IDS.map((stage) => [
41
+ stage,
42
+ {
43
+ path: resolveStageArtifactPath(input.projectRoot, input.config, stage),
44
+ status: "pending",
45
+ updatedAt: null,
46
+ },
47
+ ]));
48
+ return {
49
+ version: input.config.version,
50
+ serviceName: input.config.serviceName,
51
+ configPath: input.configPath,
52
+ projectRoot: input.projectRoot,
53
+ artifactDir,
54
+ initializedAt: timestamp,
55
+ updatedAt: timestamp,
56
+ activeStage: null,
57
+ stageOrder: [...input.config.workflow.stageOrder],
58
+ stages,
59
+ roles: { ...input.config.roles },
60
+ };
61
+ }
62
+ async function readStateFile(statePath) {
63
+ try {
64
+ const raw = await readFile(statePath, "utf8");
65
+ return JSON.parse(raw);
66
+ }
67
+ catch (error) {
68
+ if (error.code === "ENOENT") {
69
+ return null;
70
+ }
71
+ throw error;
72
+ }
73
+ }
74
+ async function writeStateFile(statePath, state) {
75
+ await writeFile(statePath, JSON.stringify(state, null, 2) + "\n", "utf8");
76
+ }
77
+ export async function ensureArtifactLayout(input) {
78
+ const artifactDir = resolveArtifactDir(input.projectRoot, input.config);
79
+ const statePath = resolveStatePath(input.projectRoot, input.config);
80
+ const createdFiles = [];
81
+ await mkdir(artifactDir, { recursive: true });
82
+ for (const stage of AKANE_STAGE_IDS) {
83
+ const artifactPath = resolveStageArtifactPath(input.projectRoot, input.config, stage);
84
+ try {
85
+ if (input.force) {
86
+ throw Object.assign(new Error("force"), { code: "ENOENT" });
87
+ }
88
+ await readFile(artifactPath, "utf8");
89
+ }
90
+ catch (error) {
91
+ if (error.code !== "ENOENT") {
92
+ throw error;
93
+ }
94
+ await writeFile(artifactPath, defaultStageTemplate(stage), "utf8");
95
+ createdFiles.push(artifactPath);
96
+ }
97
+ }
98
+ let state = await readStateFile(statePath);
99
+ if (!state || input.force) {
100
+ state = createInitialState({
101
+ config: input.config,
102
+ configPath: input.configPath,
103
+ projectRoot: input.projectRoot,
104
+ });
105
+ for (const stage of AKANE_STAGE_IDS) {
106
+ state.stages[stage].status = "initialized";
107
+ state.stages[stage].updatedAt = state.initializedAt;
108
+ }
109
+ await writeStateFile(statePath, state);
110
+ createdFiles.push(statePath);
111
+ }
112
+ return { artifactDir, statePath, state, createdFiles };
113
+ }
114
+ export async function writeStageArtifact(input) {
115
+ const ensured = await ensureArtifactLayout({
116
+ projectRoot: input.projectRoot,
117
+ config: input.config,
118
+ configPath: input.configPath,
119
+ });
120
+ const artifactPath = resolveStageArtifactPath(input.projectRoot, input.config, input.stage);
121
+ const previous = input.mode === "append"
122
+ ? await readFile(artifactPath, "utf8").catch((error) => error.code === "ENOENT" ? "" : Promise.reject(error))
123
+ : "";
124
+ const nextContent = input.mode === "append"
125
+ ? `${previous}${previous.endsWith("\n") || !previous ? "" : "\n"}${input.content}\n`
126
+ : `${input.content}${input.content.endsWith("\n") ? "" : "\n"}`;
127
+ await writeFile(artifactPath, nextContent, "utf8");
128
+ const state = ensured.state;
129
+ const timestamp = nowIso();
130
+ state.activeStage = input.stage;
131
+ state.updatedAt = timestamp;
132
+ state.stages[input.stage] = {
133
+ path: artifactPath,
134
+ status: "completed",
135
+ updatedAt: timestamp,
136
+ };
137
+ await writeStateFile(ensured.statePath, state);
138
+ return {
139
+ artifactDir: ensured.artifactDir,
140
+ artifactPath,
141
+ statePath: ensured.statePath,
142
+ state,
143
+ };
144
+ }
@@ -0,0 +1,9 @@
1
+ import type { AkaneConfig, LoadedAkaneConfig } from "./types.js";
2
+ type DeepPartial<T> = {
3
+ [K in keyof T]?: T[K] extends Array<infer U> ? U[] : T[K] extends Record<string, unknown> ? DeepPartial<T[K]> : T[K];
4
+ };
5
+ export declare function expandHome(inputPath: string): string;
6
+ export declare function defaultAkaneConfig(): AkaneConfig;
7
+ export declare function mergeAkaneConfig(base: AkaneConfig, overrides: DeepPartial<AkaneConfig>): AkaneConfig;
8
+ export declare function loadAkaneConfig(configPath?: string): Promise<LoadedAkaneConfig>;
9
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,132 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import os from "node:os";
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";
5
+ function isRecord(value) {
6
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7
+ }
8
+ export function expandHome(inputPath) {
9
+ if (inputPath === "~") {
10
+ return os.homedir();
11
+ }
12
+ if (inputPath.startsWith("~/")) {
13
+ return path.join(os.homedir(), inputPath.slice(2));
14
+ }
15
+ return inputPath;
16
+ }
17
+ export function defaultAkaneConfig() {
18
+ return {
19
+ version: 1,
20
+ serviceName: AKANE_SERVICE_NAME,
21
+ pluginOutputPath: DEFAULT_PLUGIN_OUTPUT_PATH,
22
+ globalConfigPath: DEFAULT_GLOBAL_CONFIG_PATH,
23
+ artifacts: {
24
+ dir: DEFAULT_ARTIFACT_DIR,
25
+ stateFile: DEFAULT_STATE_FILE,
26
+ files: { ...DEFAULT_STAGE_FILES },
27
+ },
28
+ workflow: {
29
+ stageOrder: [...DEFAULT_STAGE_ORDER],
30
+ },
31
+ roles: { ...DEFAULT_ROLE_MODELS },
32
+ };
33
+ }
34
+ function parseJsonFile(content, filePath) {
35
+ try {
36
+ return JSON.parse(content);
37
+ }
38
+ catch (error) {
39
+ const message = error instanceof Error ? error.message : "unknown JSON parse error";
40
+ throw new Error(`Failed to parse Akane config at ${filePath}: ${message}`);
41
+ }
42
+ }
43
+ function normalizeRoles(input, fallback) {
44
+ if (!isRecord(input)) {
45
+ return { ...fallback };
46
+ }
47
+ const next = { ...fallback };
48
+ for (const role of Object.keys(fallback)) {
49
+ const candidate = input[role];
50
+ if (typeof candidate === "string" && candidate.trim()) {
51
+ next[role] = candidate.trim();
52
+ }
53
+ }
54
+ return next;
55
+ }
56
+ function normalizeFiles(input, fallback) {
57
+ if (!isRecord(input)) {
58
+ return { ...fallback };
59
+ }
60
+ const next = { ...fallback };
61
+ for (const stage of AKANE_STAGE_IDS) {
62
+ const candidate = input[stage];
63
+ if (typeof candidate === "string" && candidate.trim()) {
64
+ next[stage] = candidate.trim();
65
+ }
66
+ }
67
+ return next;
68
+ }
69
+ function normalizeStageOrder(input, fallback) {
70
+ if (!Array.isArray(input)) {
71
+ return [...fallback];
72
+ }
73
+ const values = input.filter((value) => typeof value === "string" &&
74
+ AKANE_STAGE_IDS.includes(value));
75
+ return values.length === AKANE_STAGE_IDS.length ? values : [...fallback];
76
+ }
77
+ export function mergeAkaneConfig(base, overrides) {
78
+ return {
79
+ version: typeof overrides.version === "number" ? overrides.version : base.version,
80
+ serviceName: typeof overrides.serviceName === "string" && overrides.serviceName.trim()
81
+ ? overrides.serviceName.trim()
82
+ : base.serviceName,
83
+ pluginOutputPath: typeof overrides.pluginOutputPath === "string" &&
84
+ overrides.pluginOutputPath.trim()
85
+ ? overrides.pluginOutputPath.trim()
86
+ : base.pluginOutputPath,
87
+ globalConfigPath: typeof overrides.globalConfigPath === "string" &&
88
+ overrides.globalConfigPath.trim()
89
+ ? overrides.globalConfigPath.trim()
90
+ : base.globalConfigPath,
91
+ artifacts: {
92
+ dir: isRecord(overrides.artifacts) &&
93
+ typeof overrides.artifacts.dir === "string" &&
94
+ overrides.artifacts.dir.trim()
95
+ ? overrides.artifacts.dir.trim()
96
+ : base.artifacts.dir,
97
+ stateFile: isRecord(overrides.artifacts) &&
98
+ typeof overrides.artifacts.stateFile === "string" &&
99
+ overrides.artifacts.stateFile.trim()
100
+ ? overrides.artifacts.stateFile.trim()
101
+ : base.artifacts.stateFile,
102
+ files: normalizeFiles(isRecord(overrides.artifacts) ? overrides.artifacts.files : undefined, base.artifacts.files),
103
+ },
104
+ workflow: {
105
+ stageOrder: normalizeStageOrder(isRecord(overrides.workflow) ? overrides.workflow.stageOrder : undefined, base.workflow.stageOrder),
106
+ },
107
+ roles: normalizeRoles(overrides.roles, base.roles),
108
+ };
109
+ }
110
+ export async function loadAkaneConfig(configPath = process.env.AKANE_CONFIG_PATH ?? DEFAULT_GLOBAL_CONFIG_PATH) {
111
+ const resolvedPath = expandHome(configPath);
112
+ const defaults = defaultAkaneConfig();
113
+ try {
114
+ const raw = await readFile(resolvedPath, "utf8");
115
+ const parsed = parseJsonFile(raw, resolvedPath);
116
+ return {
117
+ path: resolvedPath,
118
+ exists: true,
119
+ config: mergeAkaneConfig(defaults, parsed),
120
+ };
121
+ }
122
+ catch (error) {
123
+ if (error.code === "ENOENT") {
124
+ return {
125
+ path: resolvedPath,
126
+ exists: false,
127
+ config: defaults,
128
+ };
129
+ }
130
+ throw error;
131
+ }
132
+ }
@@ -0,0 +1,26 @@
1
+ export declare const AKANE_SERVICE_NAME = "Akane";
2
+ export declare const DEFAULT_GLOBAL_CONFIG_PATH = "~/.config/opencode/akane.json";
3
+ export declare const DEFAULT_PLUGIN_OUTPUT_PATH = "~/.config/opencode/plugins/akane.js";
4
+ export declare const DEFAULT_ARTIFACT_DIR = ".opencode/akane";
5
+ export declare const DEFAULT_STATE_FILE = "state.json";
6
+ export declare const AKANE_STAGE_IDS: readonly ["plan", "plan-review", "implementation-context", "review-codex", "review-claude", "final-synthesis"];
7
+ export declare const AKANE_ROLE_IDS: readonly ["planner", "plan_reviewer", "implementer", "consultant_primary", "consultant_secondary", "reviewer_codex", "reviewer_claude", "synthesizer"];
8
+ export declare const DEFAULT_ROLE_MODELS: {
9
+ readonly planner: "anthropic/claude-opus-4-6";
10
+ readonly plan_reviewer: "openai/gpt-5.4";
11
+ readonly implementer: "openai/gpt-5.4";
12
+ readonly consultant_primary: "anthropic/claude-opus-4-6";
13
+ readonly consultant_secondary: "anthropic/claude-sonnet-4-6";
14
+ readonly reviewer_codex: "openai/gpt-5.4";
15
+ readonly reviewer_claude: "anthropic/claude-opus-4-6";
16
+ readonly synthesizer: "openai/gpt-5.4";
17
+ };
18
+ export declare const DEFAULT_STAGE_FILES: {
19
+ readonly plan: "plan.md";
20
+ readonly "plan-review": "plan-review.md";
21
+ readonly "implementation-context": "implementation-context.md";
22
+ readonly "review-codex": "review-codex.md";
23
+ readonly "review-claude": "review-claude.md";
24
+ readonly "final-synthesis": "final-synthesis.md";
25
+ };
26
+ export declare const DEFAULT_STAGE_ORDER: ("plan" | "plan-review" | "implementation-context" | "review-codex" | "review-claude" | "final-synthesis")[];
@@ -0,0 +1,42 @@
1
+ export const AKANE_SERVICE_NAME = "Akane";
2
+ export const DEFAULT_GLOBAL_CONFIG_PATH = "~/.config/opencode/akane.json";
3
+ export const DEFAULT_PLUGIN_OUTPUT_PATH = "~/.config/opencode/plugins/akane.js";
4
+ export const DEFAULT_ARTIFACT_DIR = ".opencode/akane";
5
+ export const DEFAULT_STATE_FILE = "state.json";
6
+ export const AKANE_STAGE_IDS = [
7
+ "plan",
8
+ "plan-review",
9
+ "implementation-context",
10
+ "review-codex",
11
+ "review-claude",
12
+ "final-synthesis",
13
+ ];
14
+ export const AKANE_ROLE_IDS = [
15
+ "planner",
16
+ "plan_reviewer",
17
+ "implementer",
18
+ "consultant_primary",
19
+ "consultant_secondary",
20
+ "reviewer_codex",
21
+ "reviewer_claude",
22
+ "synthesizer",
23
+ ];
24
+ export const DEFAULT_ROLE_MODELS = {
25
+ planner: "anthropic/claude-opus-4-6",
26
+ plan_reviewer: "openai/gpt-5.4",
27
+ implementer: "openai/gpt-5.4",
28
+ consultant_primary: "anthropic/claude-opus-4-6",
29
+ consultant_secondary: "anthropic/claude-sonnet-4-6",
30
+ reviewer_codex: "openai/gpt-5.4",
31
+ reviewer_claude: "anthropic/claude-opus-4-6",
32
+ synthesizer: "openai/gpt-5.4",
33
+ };
34
+ export const DEFAULT_STAGE_FILES = {
35
+ plan: "plan.md",
36
+ "plan-review": "plan-review.md",
37
+ "implementation-context": "implementation-context.md",
38
+ "review-codex": "review-codex.md",
39
+ "review-claude": "review-claude.md",
40
+ "final-synthesis": "final-synthesis.md",
41
+ };
42
+ export const DEFAULT_STAGE_ORDER = [...AKANE_STAGE_IDS];
@@ -0,0 +1 @@
1
+ export { AkanePlugin as default, AkanePlugin } from "./plugin.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { AkanePlugin as default, AkanePlugin } from "./plugin.js";
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ export declare const AkanePlugin: Plugin;
3
+ export default AkanePlugin;
package/dist/plugin.js ADDED
@@ -0,0 +1,20 @@
1
+ import { loadAkaneConfig } from "./config.js";
2
+ import { resolveArtifactDir } from "./artifacts.js";
3
+ import { createAkaneInitTool } from "./tools/akane-init.js";
4
+ import { createAkaneStageArtifactTool } from "./tools/akane-stage-artifact.js";
5
+ export const AkanePlugin = async (input) => {
6
+ const configInfo = await loadAkaneConfig();
7
+ return {
8
+ tool: {
9
+ akane_init: createAkaneInitTool(configInfo),
10
+ akane_stage_artifact: createAkaneStageArtifactTool(configInfo),
11
+ },
12
+ "shell.env": async (_event, output) => {
13
+ const projectRoot = input.worktree || input.directory;
14
+ output.env.AKANE_PROJECT_ROOT = projectRoot;
15
+ output.env.AKANE_ARTIFACT_DIR = resolveArtifactDir(projectRoot, configInfo.config);
16
+ output.env.AKANE_CONFIG_PATH = configInfo.path;
17
+ },
18
+ };
19
+ };
20
+ export default AkanePlugin;
@@ -0,0 +1,12 @@
1
+ import type { LoadedAkaneConfig } from "../types.js";
2
+ export declare function createAkaneInitTool(configInfo: LoadedAkaneConfig): {
3
+ description: string;
4
+ args: {
5
+ force: import("zod").ZodDefault<import("zod").ZodBoolean>;
6
+ projectRoot: import("zod").ZodOptional<import("zod").ZodString>;
7
+ };
8
+ execute(args: {
9
+ force: boolean;
10
+ projectRoot?: string | undefined;
11
+ }, context: import("@opencode-ai/plugin/tool").ToolContext): Promise<string>;
12
+ };
@@ -0,0 +1,53 @@
1
+ import { tool } from "@opencode-ai/plugin/tool";
2
+ import { ensureArtifactLayout, resolveProjectRoot } from "../artifacts.js";
3
+ export function createAkaneInitTool(configInfo) {
4
+ return tool({
5
+ description: "Initialize the per-project .opencode/akane workspace and create the stage artifact files.",
6
+ args: {
7
+ force: tool.schema
8
+ .boolean()
9
+ .default(false)
10
+ .describe("Recreate artifact files and state.json even when they already exist."),
11
+ projectRoot: tool.schema
12
+ .string()
13
+ .optional()
14
+ .describe("Optional project root override. Defaults to the current session worktree."),
15
+ },
16
+ async execute(args, context) {
17
+ const projectRoot = resolveProjectRoot({
18
+ directory: context.directory,
19
+ worktree: context.worktree,
20
+ projectRoot: args.projectRoot,
21
+ });
22
+ const result = await ensureArtifactLayout({
23
+ projectRoot,
24
+ config: configInfo.config,
25
+ configPath: configInfo.path,
26
+ force: args.force,
27
+ });
28
+ context.metadata({
29
+ title: "Akane initialized",
30
+ metadata: {
31
+ projectRoot,
32
+ artifactDir: result.artifactDir,
33
+ configPath: configInfo.path,
34
+ configFound: configInfo.exists,
35
+ createdFiles: result.createdFiles,
36
+ },
37
+ });
38
+ const lines = [
39
+ `Initialized ${configInfo.config.serviceName} in ${projectRoot}.`,
40
+ `Artifact directory: ${result.artifactDir}`,
41
+ `State file: ${result.statePath}`,
42
+ `Config path: ${configInfo.path}${configInfo.exists ? "" : " (default config fallback)"}`,
43
+ ];
44
+ if (result.createdFiles.length > 0) {
45
+ lines.push(`Created files: ${result.createdFiles.length}`);
46
+ }
47
+ else {
48
+ lines.push("No files were recreated.");
49
+ }
50
+ return lines.join("\n");
51
+ },
52
+ });
53
+ }
@@ -0,0 +1,26 @@
1
+ import type { LoadedAkaneConfig } from "../types.js";
2
+ export declare function createAkaneStageArtifactTool(configInfo: LoadedAkaneConfig): {
3
+ description: string;
4
+ args: {
5
+ stage: import("zod").ZodEnum<{
6
+ plan: "plan";
7
+ "plan-review": "plan-review";
8
+ "implementation-context": "implementation-context";
9
+ "review-codex": "review-codex";
10
+ "review-claude": "review-claude";
11
+ "final-synthesis": "final-synthesis";
12
+ }>;
13
+ content: import("zod").ZodString;
14
+ mode: import("zod").ZodDefault<import("zod").ZodEnum<{
15
+ append: "append";
16
+ replace: "replace";
17
+ }>>;
18
+ projectRoot: import("zod").ZodOptional<import("zod").ZodString>;
19
+ };
20
+ execute(args: {
21
+ stage: "plan" | "plan-review" | "implementation-context" | "review-codex" | "review-claude" | "final-synthesis";
22
+ content: string;
23
+ mode: "append" | "replace";
24
+ projectRoot?: string | undefined;
25
+ }, context: import("@opencode-ai/plugin/tool").ToolContext): Promise<string>;
26
+ };
@@ -0,0 +1,55 @@
1
+ import { tool } from "@opencode-ai/plugin/tool";
2
+ import { AKANE_STAGE_IDS } from "../constants.js";
3
+ import { resolveProjectRoot, writeStageArtifact } from "../artifacts.js";
4
+ export function createAkaneStageArtifactTool(configInfo) {
5
+ return tool({
6
+ description: "Write deterministic stage artifacts into .opencode/akane and update Akane state.json.",
7
+ args: {
8
+ stage: tool.schema
9
+ .enum(AKANE_STAGE_IDS)
10
+ .describe("The workflow stage whose artifact file should be written."),
11
+ content: tool.schema
12
+ .string()
13
+ .describe("Markdown content to write into the selected stage artifact."),
14
+ mode: tool.schema
15
+ .enum(["replace", "append"])
16
+ .default("replace")
17
+ .describe("Replace the artifact content or append to the existing file."),
18
+ projectRoot: tool.schema
19
+ .string()
20
+ .optional()
21
+ .describe("Optional project root override. Defaults to the current session worktree."),
22
+ },
23
+ async execute(args, context) {
24
+ const projectRoot = resolveProjectRoot({
25
+ directory: context.directory,
26
+ worktree: context.worktree,
27
+ projectRoot: args.projectRoot,
28
+ });
29
+ const result = await writeStageArtifact({
30
+ projectRoot,
31
+ config: configInfo.config,
32
+ configPath: configInfo.path,
33
+ stage: args.stage,
34
+ content: args.content,
35
+ mode: args.mode,
36
+ });
37
+ context.metadata({
38
+ title: `Akane stage updated: ${args.stage}`,
39
+ metadata: {
40
+ stage: args.stage,
41
+ mode: args.mode,
42
+ artifactPath: result.artifactPath,
43
+ statePath: result.statePath,
44
+ projectRoot,
45
+ },
46
+ });
47
+ return [
48
+ `Updated stage ${args.stage}.`,
49
+ `Artifact: ${result.artifactPath}`,
50
+ `State: ${result.statePath}`,
51
+ `Mode: ${args.mode}`,
52
+ ].join("\n");
53
+ },
54
+ });
55
+ }
@@ -0,0 +1,46 @@
1
+ import type { AKANE_ROLE_IDS, AKANE_STAGE_IDS, DEFAULT_ROLE_MODELS, DEFAULT_STAGE_FILES } from "./constants.js";
2
+ export type AkaneStageId = (typeof AKANE_STAGE_IDS)[number];
3
+ export type AkaneRoleId = (typeof AKANE_ROLE_IDS)[number];
4
+ export type AkaneRoles = Record<AkaneRoleId, string>;
5
+ export type AkaneStageFiles = Record<AkaneStageId, string>;
6
+ export interface AkaneConfig {
7
+ version: number;
8
+ serviceName: string;
9
+ pluginOutputPath: string;
10
+ globalConfigPath: string;
11
+ artifacts: {
12
+ dir: string;
13
+ stateFile: string;
14
+ files: AkaneStageFiles;
15
+ };
16
+ workflow: {
17
+ stageOrder: AkaneStageId[];
18
+ };
19
+ roles: AkaneRoles;
20
+ }
21
+ export interface LoadedAkaneConfig {
22
+ path: string;
23
+ exists: boolean;
24
+ config: AkaneConfig;
25
+ }
26
+ export interface AkaneStageState {
27
+ path: string;
28
+ status: "pending" | "initialized" | "completed";
29
+ updatedAt: string | null;
30
+ }
31
+ export interface AkaneState {
32
+ version: number;
33
+ serviceName: string;
34
+ configPath: string;
35
+ projectRoot: string;
36
+ artifactDir: string;
37
+ initializedAt: string;
38
+ updatedAt: string;
39
+ activeStage: AkaneStageId | null;
40
+ stageOrder: AkaneStageId[];
41
+ stages: Record<AkaneStageId, AkaneStageState>;
42
+ roles: AkaneRoles;
43
+ }
44
+ export type ArtifactWriteMode = "append" | "replace";
45
+ export type RoleModelDefaults = typeof DEFAULT_ROLE_MODELS;
46
+ export type StageFileDefaults = typeof DEFAULT_STAGE_FILES;
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ {
2
+ "version": 1,
3
+ "serviceName": "Akane",
4
+ "pluginOutputPath": "~/.config/opencode/plugins/akane.js",
5
+ "globalConfigPath": "~/.config/opencode/akane.json",
6
+ "artifacts": {
7
+ "dir": ".opencode/akane",
8
+ "stateFile": "state.json",
9
+ "files": {
10
+ "plan": "plan.md",
11
+ "plan-review": "plan-review.md",
12
+ "implementation-context": "implementation-context.md",
13
+ "review-codex": "review-codex.md",
14
+ "review-claude": "review-claude.md",
15
+ "final-synthesis": "final-synthesis.md"
16
+ }
17
+ },
18
+ "workflow": {
19
+ "stageOrder": [
20
+ "plan",
21
+ "plan-review",
22
+ "implementation-context",
23
+ "review-codex",
24
+ "review-claude",
25
+ "final-synthesis"
26
+ ]
27
+ },
28
+ "roles": {
29
+ "planner": "anthropic/claude-opus-4-6",
30
+ "plan_reviewer": "openai/gpt-5.4",
31
+ "implementer": "openai/gpt-5.4",
32
+ "consultant_primary": "anthropic/claude-opus-4-6",
33
+ "consultant_secondary": "anthropic/claude-sonnet-4-6",
34
+ "reviewer_codex": "openai/gpt-5.4",
35
+ "reviewer_claude": "anthropic/claude-opus-4-6",
36
+ "synthesizer": "openai/gpt-5.4"
37
+ }
38
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "opencode-akane",
3
+ "version": "0.1.0",
4
+ "description": "Akane orchestration plugin for OpenCode",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "keywords": [
8
+ "opencode",
9
+ "plugin",
10
+ "akane",
11
+ "automation"
12
+ ],
13
+ "homepage": "https://github.com/NaturaAurum/akane-agents#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/NaturaAurum/akane-agents/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/NaturaAurum/akane-agents.git"
20
+ },
21
+ "main": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md",
32
+ "examples/akane.example.json"
33
+ ],
34
+ "engines": {
35
+ "node": ">=22"
36
+ },
37
+ "publishConfig": {
38
+ "registry": "https://registry.npmjs.org/"
39
+ },
40
+ "scripts": {
41
+ "clean": "rm -rf dist",
42
+ "build": "tsc -p tsconfig.json",
43
+ "typecheck": "tsc --noEmit -p tsconfig.json",
44
+ "pack:check": "npm pack --dry-run",
45
+ "prepublishOnly": "bun run clean && bun run build && bun run typecheck"
46
+ },
47
+ "dependencies": {
48
+ "@opencode-ai/plugin": "1.2.20"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^24.12.0",
52
+ "typescript": "^5.9.3"
53
+ }
54
+ }