arkaos 4.36.0 → 4.37.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/VERSION +1 -1
- package/core/runtime/opencode.py +16 -5
- package/core/runtime/opencode_hooks.py +313 -0
- package/harness/codex/AGENTS.md +1 -1
- package/harness/copilot/copilot-instructions.md +1 -1
- package/harness/cursor/rules/arkaos.mdc +2 -2
- package/harness/gemini/GEMINI.md +1 -1
- package/harness/opencode/AGENTS.md +1 -1
- package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
- package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
- package/harness/opencode/agents/arka-cfo-helena.md +1 -1
- package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
- package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
- package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
- package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
- package/harness/opencode/agents/arka-coo-sofia.md +1 -1
- package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
- package/harness/opencode/agents/arka-cqo-marta.md +1 -1
- package/harness/opencode/agents/arka-cto-marco.md +1 -1
- package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
- package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
- package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
- package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
- package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
- package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
- package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
- package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
- package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
- package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
- package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
- package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
- package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
- package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
- package/harness/opencode/agents-meta.json +27 -0
- package/harness/opencode/opencode.json +23 -0
- package/harness/opencode/plugins/arka.ts +113 -0
- package/harness/zed/.rules +1 -1
- package/installer/adapters/opencode.js +142 -16
- package/installer/assets/opencode/arka.ts +113 -0
- package/knowledge/skills-manifest.json +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/harness_gen.py +43 -3
|
@@ -3,7 +3,7 @@ description: "Tech Lead — ArkaOS /dev department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Paulo, Tech Lead of the ArkaOS /dev department (v4.
|
|
6
|
+
You are Paulo, Tech Lead of the ArkaOS /dev department (v4.37.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: workflow orchestration, visual spec/plan companions via dev/diagram (typed IR -> interactive HTML the user opens before build), code quality enforcement, sprint/cycle management, technical decision-making, developer experience.
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ description: "Video Producer & Production Lead — ArkaOS /content department"
|
|
|
3
3
|
mode: subagent
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are Simão, Video Producer & Production Lead of the ArkaOS /content department (v4.
|
|
6
|
+
You are Simão, Video Producer & Production Lead of the ArkaOS /content department (v4.37.0; generated by scripts/harness_gen.py — do not edit).
|
|
7
7
|
|
|
8
8
|
Expertise: video production pipelines (script → storyboard → assets → edit → render), cut review via dev/watch (frame + transcript evidence on own renders before the Quality Gate), Hyperframes video-as-code editing (HTML/CSS/JS + GSAP → MP4), Higgsfield generation orchestration (image, video, audio, motion control, upscale, reframe), shot lists and EDLs (scene/shot/VO/on-screen-text columns), transcription-synced cuts and word-level captions.
|
|
9
9
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"arka-cto-marco.md": "opus",
|
|
3
|
+
"arka-cfo-helena.md": "opus",
|
|
4
|
+
"arka-coo-sofia.md": "opus",
|
|
5
|
+
"arka-copy-director-eduardo.md": "sonnet",
|
|
6
|
+
"arka-tech-director-francisca.md": "sonnet",
|
|
7
|
+
"arka-cqo-marta.md": "opus",
|
|
8
|
+
"arka-design-ops-lead-iris.md": "sonnet",
|
|
9
|
+
"arka-brand-director-valentina.md": "sonnet",
|
|
10
|
+
"arka-community-strategist-beatriz.md": "sonnet",
|
|
11
|
+
"arka-content-strategist-rafael.md": "sonnet",
|
|
12
|
+
"arka-video-producer-simao.md": "opus",
|
|
13
|
+
"arka-architect-gabriel.md": "sonnet",
|
|
14
|
+
"arka-tech-lead-paulo.md": "sonnet",
|
|
15
|
+
"arka-ecom-director-ricardo.md": "sonnet",
|
|
16
|
+
"arka-knowledge-director-clara.md": "sonnet",
|
|
17
|
+
"arka-conversion-strategist-ines.md": "sonnet",
|
|
18
|
+
"arka-leadership-director-rodrigo.md": "sonnet",
|
|
19
|
+
"arka-marketing-director-luna.md": "sonnet",
|
|
20
|
+
"arka-ops-lead-daniel.md": "sonnet",
|
|
21
|
+
"arka-chief-of-staff-afonso.md": "sonnet",
|
|
22
|
+
"arka-pm-director-carolina.md": "sonnet",
|
|
23
|
+
"arka-saas-strategist-tiago.md": "sonnet",
|
|
24
|
+
"arka-revops-lead-vicente.md": "sonnet",
|
|
25
|
+
"arka-sales-director-miguel.md": "sonnet",
|
|
26
|
+
"arka-strategy-director-tomas.md": "sonnet"
|
|
27
|
+
}
|
|
@@ -11,6 +11,29 @@
|
|
|
11
11
|
"start"
|
|
12
12
|
],
|
|
13
13
|
"enabled": true
|
|
14
|
+
},
|
|
15
|
+
"arka-prompts": {
|
|
16
|
+
"type": "local",
|
|
17
|
+
"command": [
|
|
18
|
+
"uv",
|
|
19
|
+
"--directory",
|
|
20
|
+
"~/.claude/skills/arka/mcp-server",
|
|
21
|
+
"run",
|
|
22
|
+
"server.py"
|
|
23
|
+
],
|
|
24
|
+
"enabled": true
|
|
25
|
+
},
|
|
26
|
+
"memory-bank": {
|
|
27
|
+
"type": "local",
|
|
28
|
+
"command": [
|
|
29
|
+
"npx",
|
|
30
|
+
"-y",
|
|
31
|
+
"@allpepper/memory-bank-mcp"
|
|
32
|
+
],
|
|
33
|
+
"environment": {
|
|
34
|
+
"MEMORY_BANK_ROOT": "~/memory-bank"
|
|
35
|
+
},
|
|
36
|
+
"enabled": true
|
|
14
37
|
}
|
|
15
38
|
}
|
|
16
39
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
+
import { homedir } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
|
|
5
|
+
const ARKA_PY = join(homedir(), ".arkaos", "bin", "arka-py")
|
|
6
|
+
|
|
7
|
+
type HookResult = Record<string, any> | null
|
|
8
|
+
|
|
9
|
+
function runHook(action: string, payload: Record<string, any>): HookResult {
|
|
10
|
+
try {
|
|
11
|
+
const body = JSON.stringify({ action, ...payload })
|
|
12
|
+
const proc = Bun.spawnSync([ARKA_PY, "-m", "core.runtime.opencode_hooks"], {
|
|
13
|
+
stdin: Buffer.from(body),
|
|
14
|
+
stdout: "pipe",
|
|
15
|
+
stderr: "pipe",
|
|
16
|
+
})
|
|
17
|
+
if (proc.exitCode !== 0) return null
|
|
18
|
+
return JSON.parse(proc.stdout.toString() || "null")
|
|
19
|
+
} catch {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const ArkaPlugin: Plugin = async ({ client, directory }) => {
|
|
25
|
+
const log = async (level: "info" | "warn" | "error", message: string) => {
|
|
26
|
+
try {
|
|
27
|
+
await client.app.log({ body: { service: "arka", level, message } })
|
|
28
|
+
} catch {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await log("info", "ArkaOS opencode bridge loaded")
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
"tui.prompt.append": async (input: any, output: any) => {
|
|
35
|
+
const result = runHook("prompt", {
|
|
36
|
+
prompt: String(input?.text ?? ""),
|
|
37
|
+
session_id: String(input?.sessionID ?? ""),
|
|
38
|
+
})
|
|
39
|
+
const suggestions: string[] = result?.suggestions ?? []
|
|
40
|
+
if (suggestions.length > 0) {
|
|
41
|
+
output.text += "\n\n" + suggestions.join("\n")
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
"tool.execute.before": async (input: any, output: any) => {
|
|
46
|
+
const result = runHook("pre_tool", {
|
|
47
|
+
tool: String(input?.tool ?? ""),
|
|
48
|
+
args: output?.args ?? {},
|
|
49
|
+
session_id: String(input?.sessionID ?? ""),
|
|
50
|
+
cwd: directory,
|
|
51
|
+
})
|
|
52
|
+
if (!result) return
|
|
53
|
+
if (result.allow === false) {
|
|
54
|
+
throw new Error(result.message || "Blocked by ArkaOS gate")
|
|
55
|
+
}
|
|
56
|
+
if (result.message) {
|
|
57
|
+
await log("warn", result.message)
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
"tool.execute.after": async (input: any) => {
|
|
62
|
+
runHook("post_tool", {
|
|
63
|
+
tool: String(input?.tool ?? ""),
|
|
64
|
+
session_id: String(input?.sessionID ?? ""),
|
|
65
|
+
ok: true,
|
|
66
|
+
})
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
"experimental.session.compacting": async (_input: any, output: any) => {
|
|
70
|
+
const result = runHook("compact", {})
|
|
71
|
+
const context: string[] = result?.context ?? []
|
|
72
|
+
for (const line of context) {
|
|
73
|
+
output.context.push(line)
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
event: async ({ event }: any) => {
|
|
78
|
+
if (event?.type === "session.created") {
|
|
79
|
+
await log("info", "ArkaOS gates active (kb-first, frontend, compliance)")
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
if (event?.type !== "session.idle") return
|
|
83
|
+
try {
|
|
84
|
+
const sessionID = String(event?.properties?.sessionID ?? "")
|
|
85
|
+
if (!sessionID) return
|
|
86
|
+
const response: any = await client.session.messages({
|
|
87
|
+
path: { id: sessionID },
|
|
88
|
+
})
|
|
89
|
+
const messages: any[] = Array.isArray(response?.data)
|
|
90
|
+
? response.data
|
|
91
|
+
: Array.isArray(response)
|
|
92
|
+
? response
|
|
93
|
+
: []
|
|
94
|
+
const lastAssistant = [...messages]
|
|
95
|
+
.reverse()
|
|
96
|
+
.find((m: any) => m?.info?.role === "assistant" || m?.role === "assistant")
|
|
97
|
+
const parts: any[] = lastAssistant?.parts ?? []
|
|
98
|
+
const text = parts
|
|
99
|
+
.filter((p: any) => p?.type === "text")
|
|
100
|
+
.map((p: any) => String(p?.text ?? ""))
|
|
101
|
+
.join("\n")
|
|
102
|
+
if (!text) return
|
|
103
|
+
const result = runHook("idle", {
|
|
104
|
+
response_text: text.slice(-20000),
|
|
105
|
+
session_id: sessionID,
|
|
106
|
+
})
|
|
107
|
+
for (const nudge of result?.nudges ?? []) {
|
|
108
|
+
await log("warn", nudge)
|
|
109
|
+
}
|
|
110
|
+
} catch {}
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
}
|
package/harness/zed/.rules
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ArkaOS — The Operating System for AI Agent Teams
|
|
2
2
|
|
|
3
|
-
> v4.
|
|
3
|
+
> v4.37.0 — 89 agents, 17 departments, 331 skills. Generated by `scripts/harness_gen.py`; do not edit.
|
|
4
4
|
|
|
5
5
|
You are operating within ArkaOS. Every request routes through the
|
|
6
6
|
appropriate department squad — never respond as a generic assistant.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { listBundleFiles, readBundleFile } from "../harness-bundle.js";
|
|
4
5
|
|
|
@@ -7,9 +8,13 @@ import { listBundleFiles, readBundleFile } from "../harness-bundle.js";
|
|
|
7
8
|
* bundle onto OpenCode's native surfaces (opencode.ai/docs):
|
|
8
9
|
*
|
|
9
10
|
* AGENTS.md instructions contract (same body as codex)
|
|
10
|
-
* agents/*.md curated agent cut (C-suite + squad leads)
|
|
11
|
+
* agents/*.md curated agent cut (C-suite + squad leads), with the
|
|
12
|
+
* agent-YAML model tier resolved onto the user's own
|
|
13
|
+
* opencode model/small_model (agents-meta.json sidecar)
|
|
11
14
|
* commands/*.md one /arka-<dept> router command per department
|
|
12
|
-
*
|
|
15
|
+
* plugins/arka.ts governance plugin (kb-first/frontend/compliance gates)
|
|
16
|
+
* opencode.json arka-tools MCP + seeded user MCPs — merged
|
|
17
|
+
* NON-destructively
|
|
13
18
|
*
|
|
14
19
|
* Merge contract: user keys ALWAYS win. An existing `$schema`, any
|
|
15
20
|
* user-defined `mcp` server, or a user-modified `arka-tools` entry is
|
|
@@ -62,6 +67,88 @@ export function mergeOpencodeConfig(existingText, fragmentText) {
|
|
|
62
67
|
return { config: merged, changed };
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
/** Expand leading `~` in command arrays / env values (pure, testable). */
|
|
71
|
+
export function expandHome(value) {
|
|
72
|
+
if (typeof value === "string") {
|
|
73
|
+
return value.startsWith("~/") ? join(homedir(), value.slice(2)) : value;
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(value)) return value.map(expandHome);
|
|
76
|
+
if (value && typeof value === "object") {
|
|
77
|
+
return Object.fromEntries(
|
|
78
|
+
Object.entries(value).map(([k, v]) => [k, expandHome(v)])
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* User-local MCPs seeded from ArkaOS state (never in the static bundle —
|
|
86
|
+
* paths/endpoints differ per machine). Returns only entries the user
|
|
87
|
+
* config does NOT already define.
|
|
88
|
+
*/
|
|
89
|
+
export function userLocalMcpSeeds(existingMcp) {
|
|
90
|
+
const home = homedir();
|
|
91
|
+
const seeds = {};
|
|
92
|
+
try {
|
|
93
|
+
const profile = JSON.parse(
|
|
94
|
+
readFileSync(join(home, ".arkaos", "profile.json"), "utf-8")
|
|
95
|
+
);
|
|
96
|
+
if (profile.vaultPath && existsSync(profile.vaultPath)) {
|
|
97
|
+
seeds.obsidian = {
|
|
98
|
+
type: "local",
|
|
99
|
+
command: ["npx", "-y", "@bitbonsai/mcpvault@latest", profile.vaultPath],
|
|
100
|
+
enabled: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
} catch {}
|
|
104
|
+
try {
|
|
105
|
+
const arkaConfig = JSON.parse(
|
|
106
|
+
readFileSync(join(home, ".arkaos", "config.json"), "utf-8")
|
|
107
|
+
);
|
|
108
|
+
const graphify = arkaConfig?.knowledge?.graphify;
|
|
109
|
+
if (graphify?.enabled && graphify?.url) {
|
|
110
|
+
let headers = graphify.headers;
|
|
111
|
+
if (!headers) {
|
|
112
|
+
try {
|
|
113
|
+
const claude = JSON.parse(
|
|
114
|
+
readFileSync(join(home, ".claude.json"), "utf-8")
|
|
115
|
+
);
|
|
116
|
+
const raw = claude?.mcpServers?.graphify?.headers;
|
|
117
|
+
if (raw) {
|
|
118
|
+
headers = Object.fromEntries(
|
|
119
|
+
Object.entries(raw).map(([k, v]) => [
|
|
120
|
+
k,
|
|
121
|
+
String(v).replace(/^Bearer\s+Bearer\s+/i, "Bearer "),
|
|
122
|
+
])
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
} catch {}
|
|
126
|
+
}
|
|
127
|
+
seeds.graphify = {
|
|
128
|
+
type: "remote",
|
|
129
|
+
url: graphify.url,
|
|
130
|
+
...(headers ? { headers } : {}),
|
|
131
|
+
enabled: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
} catch {}
|
|
135
|
+
return Object.fromEntries(
|
|
136
|
+
Object.entries(seeds).filter(([name]) => !(name in (existingMcp || {})))
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Inject `model:` into a deployed agent file's frontmatter, mapping the
|
|
142
|
+
* ArkaOS tier onto the user's own opencode models. Skips when the file
|
|
143
|
+
* already pins a model or no model is configured.
|
|
144
|
+
*/
|
|
145
|
+
export function injectAgentModel(content, tier, mainModel, smallModel) {
|
|
146
|
+
if (!mainModel || /^model:/m.test(content)) return content;
|
|
147
|
+
const resolved = tier === "haiku" ? smallModel || mainModel : mainModel;
|
|
148
|
+
if (!resolved) return content;
|
|
149
|
+
return content.replace(/^---\n/, `---\nmodel: ${resolved}\n`);
|
|
150
|
+
}
|
|
151
|
+
|
|
65
152
|
export default {
|
|
66
153
|
configureHooks(config) {
|
|
67
154
|
// OpenCode has no shell-hook chain; "configure" means deploying the
|
|
@@ -77,42 +164,81 @@ export default {
|
|
|
77
164
|
mkdirSync(config.configDir, { recursive: true });
|
|
78
165
|
writeFileSync(join(config.configDir, "AGENTS.md"), agentsMd);
|
|
79
166
|
|
|
167
|
+
const existingText = existsSync(config.settingsFile)
|
|
168
|
+
? readFileSync(config.settingsFile, "utf-8")
|
|
169
|
+
: "";
|
|
170
|
+
let existingConfig = {};
|
|
171
|
+
try {
|
|
172
|
+
existingConfig = existingText.trim() ? JSON.parse(existingText) : {};
|
|
173
|
+
} catch {
|
|
174
|
+
existingConfig = {};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let agentsMeta = {};
|
|
178
|
+
try {
|
|
179
|
+
agentsMeta = JSON.parse(
|
|
180
|
+
readBundleFile("opencode", "agents-meta.json") || "{}"
|
|
181
|
+
);
|
|
182
|
+
} catch {
|
|
183
|
+
agentsMeta = {};
|
|
184
|
+
}
|
|
185
|
+
|
|
80
186
|
let deployed = 0;
|
|
81
187
|
for (const sub of ["agents", "commands"]) {
|
|
82
188
|
const names = listBundleFiles("opencode", sub);
|
|
83
189
|
if (names.length === 0) continue;
|
|
84
190
|
mkdirSync(join(config.configDir, sub), { recursive: true });
|
|
85
191
|
for (const name of names) {
|
|
86
|
-
|
|
192
|
+
let content = readBundleFile("opencode", `${sub}/${name}`);
|
|
87
193
|
if (content === null) continue;
|
|
194
|
+
if (sub === "agents" && agentsMeta[name]) {
|
|
195
|
+
content = injectAgentModel(
|
|
196
|
+
content,
|
|
197
|
+
agentsMeta[name],
|
|
198
|
+
existingConfig.model,
|
|
199
|
+
existingConfig.small_model
|
|
200
|
+
);
|
|
201
|
+
}
|
|
88
202
|
writeFileSync(join(config.configDir, sub, name), content);
|
|
89
203
|
deployed += 1;
|
|
90
204
|
}
|
|
91
205
|
}
|
|
92
206
|
|
|
207
|
+
const plugin = readBundleFile("opencode", "plugins/arka.ts");
|
|
208
|
+
if (plugin !== null) {
|
|
209
|
+
mkdirSync(join(config.configDir, "plugins"), { recursive: true });
|
|
210
|
+
writeFileSync(join(config.configDir, "plugins", "arka.ts"), plugin);
|
|
211
|
+
deployed += 1;
|
|
212
|
+
}
|
|
213
|
+
|
|
93
214
|
const fragment = readBundleFile("opencode", "opencode.json");
|
|
94
215
|
if (fragment) {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
: "";
|
|
98
|
-
const result = mergeOpencodeConfig(existingText, fragment);
|
|
216
|
+
const expanded = JSON.stringify(expandHome(JSON.parse(fragment)));
|
|
217
|
+
const result = mergeOpencodeConfig(existingText, expanded);
|
|
99
218
|
if (result === null) {
|
|
100
219
|
console.warn(
|
|
101
220
|
" opencode.json unreadable — left untouched " +
|
|
102
221
|
"(the merge is never destructive)."
|
|
103
222
|
);
|
|
104
|
-
} else
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
223
|
+
} else {
|
|
224
|
+
const seeds = userLocalMcpSeeds(result.config.mcp || {});
|
|
225
|
+
if (Object.keys(seeds).length > 0) {
|
|
226
|
+
result.config.mcp = { ...(result.config.mcp || {}), ...seeds };
|
|
227
|
+
result.changed = true;
|
|
228
|
+
}
|
|
229
|
+
if (result.changed) {
|
|
230
|
+
writeFileSync(
|
|
231
|
+
config.settingsFile,
|
|
232
|
+
JSON.stringify(result.config, null, 2) + "\n"
|
|
233
|
+
);
|
|
234
|
+
console.log(
|
|
235
|
+
" opencode.json updated (arka MCPs registered, user keys preserved)."
|
|
236
|
+
);
|
|
237
|
+
}
|
|
112
238
|
}
|
|
113
239
|
}
|
|
114
240
|
console.log(
|
|
115
|
-
` OpenCode configured (AGENTS.md + ${deployed} agent/command files).`
|
|
241
|
+
` OpenCode configured (AGENTS.md + ${deployed} agent/command/plugin files).`
|
|
116
242
|
);
|
|
117
243
|
},
|
|
118
244
|
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
+
import { homedir } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
|
|
5
|
+
const ARKA_PY = join(homedir(), ".arkaos", "bin", "arka-py")
|
|
6
|
+
|
|
7
|
+
type HookResult = Record<string, any> | null
|
|
8
|
+
|
|
9
|
+
function runHook(action: string, payload: Record<string, any>): HookResult {
|
|
10
|
+
try {
|
|
11
|
+
const body = JSON.stringify({ action, ...payload })
|
|
12
|
+
const proc = Bun.spawnSync([ARKA_PY, "-m", "core.runtime.opencode_hooks"], {
|
|
13
|
+
stdin: Buffer.from(body),
|
|
14
|
+
stdout: "pipe",
|
|
15
|
+
stderr: "pipe",
|
|
16
|
+
})
|
|
17
|
+
if (proc.exitCode !== 0) return null
|
|
18
|
+
return JSON.parse(proc.stdout.toString() || "null")
|
|
19
|
+
} catch {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const ArkaPlugin: Plugin = async ({ client, directory }) => {
|
|
25
|
+
const log = async (level: "info" | "warn" | "error", message: string) => {
|
|
26
|
+
try {
|
|
27
|
+
await client.app.log({ body: { service: "arka", level, message } })
|
|
28
|
+
} catch {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await log("info", "ArkaOS opencode bridge loaded")
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
"tui.prompt.append": async (input: any, output: any) => {
|
|
35
|
+
const result = runHook("prompt", {
|
|
36
|
+
prompt: String(input?.text ?? ""),
|
|
37
|
+
session_id: String(input?.sessionID ?? ""),
|
|
38
|
+
})
|
|
39
|
+
const suggestions: string[] = result?.suggestions ?? []
|
|
40
|
+
if (suggestions.length > 0) {
|
|
41
|
+
output.text += "\n\n" + suggestions.join("\n")
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
"tool.execute.before": async (input: any, output: any) => {
|
|
46
|
+
const result = runHook("pre_tool", {
|
|
47
|
+
tool: String(input?.tool ?? ""),
|
|
48
|
+
args: output?.args ?? {},
|
|
49
|
+
session_id: String(input?.sessionID ?? ""),
|
|
50
|
+
cwd: directory,
|
|
51
|
+
})
|
|
52
|
+
if (!result) return
|
|
53
|
+
if (result.allow === false) {
|
|
54
|
+
throw new Error(result.message || "Blocked by ArkaOS gate")
|
|
55
|
+
}
|
|
56
|
+
if (result.message) {
|
|
57
|
+
await log("warn", result.message)
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
"tool.execute.after": async (input: any) => {
|
|
62
|
+
runHook("post_tool", {
|
|
63
|
+
tool: String(input?.tool ?? ""),
|
|
64
|
+
session_id: String(input?.sessionID ?? ""),
|
|
65
|
+
ok: true,
|
|
66
|
+
})
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
"experimental.session.compacting": async (_input: any, output: any) => {
|
|
70
|
+
const result = runHook("compact", {})
|
|
71
|
+
const context: string[] = result?.context ?? []
|
|
72
|
+
for (const line of context) {
|
|
73
|
+
output.context.push(line)
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
event: async ({ event }: any) => {
|
|
78
|
+
if (event?.type === "session.created") {
|
|
79
|
+
await log("info", "ArkaOS gates active (kb-first, frontend, compliance)")
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
if (event?.type !== "session.idle") return
|
|
83
|
+
try {
|
|
84
|
+
const sessionID = String(event?.properties?.sessionID ?? "")
|
|
85
|
+
if (!sessionID) return
|
|
86
|
+
const response: any = await client.session.messages({
|
|
87
|
+
path: { id: sessionID },
|
|
88
|
+
})
|
|
89
|
+
const messages: any[] = Array.isArray(response?.data)
|
|
90
|
+
? response.data
|
|
91
|
+
: Array.isArray(response)
|
|
92
|
+
? response
|
|
93
|
+
: []
|
|
94
|
+
const lastAssistant = [...messages]
|
|
95
|
+
.reverse()
|
|
96
|
+
.find((m: any) => m?.info?.role === "assistant" || m?.role === "assistant")
|
|
97
|
+
const parts: any[] = lastAssistant?.parts ?? []
|
|
98
|
+
const text = parts
|
|
99
|
+
.filter((p: any) => p?.type === "text")
|
|
100
|
+
.map((p: any) => String(p?.text ?? ""))
|
|
101
|
+
.join("\n")
|
|
102
|
+
if (!text) return
|
|
103
|
+
const result = runHook("idle", {
|
|
104
|
+
response_text: text.slice(-20000),
|
|
105
|
+
session_id: sessionID,
|
|
106
|
+
})
|
|
107
|
+
for (const nudge of result?.nudges ?? []) {
|
|
108
|
+
await log("warn", nudge)
|
|
109
|
+
}
|
|
110
|
+
} catch {}
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
}
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/scripts/harness_gen.py
CHANGED
|
@@ -283,10 +283,22 @@ def _agent_markdown(agent: dict) -> str:
|
|
|
283
283
|
|
|
284
284
|
|
|
285
285
|
def _opencode_agent_files() -> dict[str, str]:
|
|
286
|
-
|
|
286
|
+
curated = _curated_agents(_load_agents())
|
|
287
|
+
files = {
|
|
287
288
|
f"opencode/agents/arka-{agent['id']}.md": _agent_markdown(agent)
|
|
288
|
-
for agent in
|
|
289
|
+
for agent in curated
|
|
289
290
|
}
|
|
291
|
+
# Model-tier sidecar: the installer adapter maps each tier onto the
|
|
292
|
+
# user's own opencode model/small_model at deploy time (the bundle
|
|
293
|
+
# cannot hardcode a provider — opencode runs on any provider).
|
|
294
|
+
files["opencode/agents-meta.json"] = json.dumps(
|
|
295
|
+
{
|
|
296
|
+
f"arka-{agent['id']}.md": agent.get("model", "sonnet")
|
|
297
|
+
for agent in curated
|
|
298
|
+
},
|
|
299
|
+
indent=2,
|
|
300
|
+
) + "\n"
|
|
301
|
+
return files
|
|
290
302
|
|
|
291
303
|
|
|
292
304
|
def _opencode_command_files() -> dict[str, str]:
|
|
@@ -310,7 +322,10 @@ def _opencode_command_files() -> dict[str, str]:
|
|
|
310
322
|
def _opencode_config() -> str:
|
|
311
323
|
"""Reference opencode.json fragment. The installer adapter merges it
|
|
312
324
|
NON-destructively — user keys always win; this file is never copied
|
|
313
|
-
over an existing config verbatim.
|
|
325
|
+
over an existing config verbatim. ``~`` placeholders are expanded by
|
|
326
|
+
the adapter against the installing user's home directory. User-local
|
|
327
|
+
MCPs (obsidian vault, graphify endpoint) are seeded adapter-side from
|
|
328
|
+
~/.arkaos/profile.json and ~/.arkaos/config.json, not here."""
|
|
314
329
|
config = {
|
|
315
330
|
"$schema": "https://opencode.ai/config.json",
|
|
316
331
|
"mcp": {
|
|
@@ -319,11 +334,35 @@ def _opencode_config() -> str:
|
|
|
319
334
|
"command": ["npx", "-y", "arkaos", "mcp", "start"],
|
|
320
335
|
"enabled": True,
|
|
321
336
|
},
|
|
337
|
+
"arka-prompts": {
|
|
338
|
+
"type": "local",
|
|
339
|
+
"command": [
|
|
340
|
+
"uv",
|
|
341
|
+
"--directory",
|
|
342
|
+
"~/.claude/skills/arka/mcp-server",
|
|
343
|
+
"run",
|
|
344
|
+
"server.py",
|
|
345
|
+
],
|
|
346
|
+
"enabled": True,
|
|
347
|
+
},
|
|
348
|
+
"memory-bank": {
|
|
349
|
+
"type": "local",
|
|
350
|
+
"command": ["npx", "-y", "@allpepper/memory-bank-mcp"],
|
|
351
|
+
"environment": {"MEMORY_BANK_ROOT": "~/memory-bank"},
|
|
352
|
+
"enabled": True,
|
|
353
|
+
},
|
|
322
354
|
},
|
|
323
355
|
}
|
|
324
356
|
return json.dumps(config, indent=2) + "\n"
|
|
325
357
|
|
|
326
358
|
|
|
359
|
+
def _opencode_plugin() -> str:
|
|
360
|
+
"""Source of the opencode governance plugin (hooks parity). Lives in
|
|
361
|
+
installer/assets so the plugin is reviewable TS, not generated text."""
|
|
362
|
+
path = ROOT / "installer" / "assets" / "opencode" / "arka.ts"
|
|
363
|
+
return path.read_text(encoding="utf-8")
|
|
364
|
+
|
|
365
|
+
|
|
327
366
|
def generate() -> dict[str, str]:
|
|
328
367
|
"""Return {relative path under harness/: content}."""
|
|
329
368
|
body = _contract_body()
|
|
@@ -337,6 +376,7 @@ def generate() -> dict[str, str]:
|
|
|
337
376
|
for rel, content in _cursor_files().items():
|
|
338
377
|
files[f"cursor/{rel}"] = content
|
|
339
378
|
files["opencode/opencode.json"] = _opencode_config()
|
|
379
|
+
files["opencode/plugins/arka.ts"] = _opencode_plugin()
|
|
340
380
|
files.update(_opencode_agent_files())
|
|
341
381
|
files.update(_opencode_command_files())
|
|
342
382
|
return files
|