@aria_asi/cli 0.2.23 → 0.2.25

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.
@@ -1 +1 @@
1
- {"version":3,"file":"opencode.d.ts","sourceRoot":"","sources":["../../../../src/connectors/opencode.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,wBAAsB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA2D3E;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAoB5D"}
1
+ {"version":3,"file":"opencode.d.ts","sourceRoot":"","sources":["../../../../src/connectors/opencode.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAiD/C,wBAAsB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAyH3E;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAqC5D"}
@@ -1,6 +1,52 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, readdirSync, statSync, chmodSync, unlinkSync, } from 'fs';
2
2
  import { homedir } from 'os';
3
3
  import * as path from 'path';
4
+ import { fileURLToPath } from 'node:url';
5
+ // ── Bundled OpenCode plugins ────────────────────────────────────────────────
6
+ //
7
+ // Two plugins ship with the connector:
8
+ // - harness-context: spawns the bundled inject-context.mjs on session start
9
+ // and prepends the resolved harness packet to OpenCode's
10
+ // system prompt. Routes through the canonical SDK at
11
+ // ~/.claude/aria-sdk/.
12
+ // - harness-role: injects the active role profile (default
13
+ // opencode_deepseek_engineer) — intro / continuity /
14
+ // post-action governance rules.
15
+ //
16
+ // Both live at <pkg>/opencode-plugins/<name>/. connectOpenCode copies them
17
+ // into ~/.opencode/plugins/<name>/ and wires the absolute paths into
18
+ // ~/.opencode/config.json's `plugin` (singular, OpenCode v2 schema) array.
19
+ //
20
+ // Compiled location of THIS file (claude-code path applies the same way):
21
+ // <pkg>/dist/aria-connector/src/connectors/opencode.js
22
+ // Plugins ship at <pkg>/opencode-plugins/ → 4 dirs up + 'opencode-plugins'.
23
+ const PLUGIN_NAMES = ['harness-context', 'harness-role'];
24
+ function packageOpenCodePluginsDir() {
25
+ const here = path.dirname(fileURLToPath(import.meta.url));
26
+ return path.resolve(here, '..', '..', '..', '..', 'opencode-plugins');
27
+ }
28
+ function copyPluginDir(srcDir, dstDir, logs) {
29
+ if (!existsSync(dstDir)) {
30
+ mkdirSync(dstDir, { recursive: true, mode: 0o755 });
31
+ }
32
+ for (const name of readdirSync(srcDir)) {
33
+ const src = path.join(srcDir, name);
34
+ const stat = statSync(src);
35
+ if (!stat.isFile())
36
+ continue;
37
+ const dst = path.join(dstDir, name);
38
+ copyFileSync(src, dst);
39
+ if (name.endsWith('.mjs') || name.endsWith('.js')) {
40
+ try {
41
+ chmodSync(dst, 0o755);
42
+ }
43
+ catch {
44
+ // chmod failure isn't fatal on filesystems that ignore mode bits.
45
+ }
46
+ }
47
+ }
48
+ logs.push(`Installed plugin → ${dstDir}`);
49
+ }
4
50
  export async function connectOpenCode(config) {
5
51
  const logs = [];
6
52
  const opencodeDir = path.join(homedir(), '.opencode');
@@ -8,17 +54,51 @@ export async function connectOpenCode(config) {
8
54
  logs.push('No ~/.opencode directory found — OpenCode may not be installed');
9
55
  return logs;
10
56
  }
57
+ // ── Install bundled plugins into ~/.opencode/plugins/<name>/ ──────────────
58
+ const pluginsRoot = path.join(opencodeDir, 'plugins');
59
+ if (!existsSync(pluginsRoot)) {
60
+ mkdirSync(pluginsRoot, { recursive: true, mode: 0o755 });
61
+ }
62
+ // Remove legacy single-file plugin shadows (~/.opencode/plugins/<name>.js).
63
+ // OpenCode v2 auto-discovers BOTH loose .js files and dir-shaped plugins;
64
+ // when both exist with the same name, behavior is undefined and on at least
65
+ // one machine the loose .js (carrying old hardcoded paths to
66
+ // ~/rei-ai-brain/harness/inject-context.mjs) was winning over the dir
67
+ // install. Idempotent cleanup so re-running connect heals these.
68
+ for (const pluginName of PLUGIN_NAMES) {
69
+ const shadowPath = path.join(pluginsRoot, `${pluginName}.js`);
70
+ if (existsSync(shadowPath)) {
71
+ try {
72
+ unlinkSync(shadowPath);
73
+ logs.push(`Removed legacy single-file plugin shadow: ${shadowPath}`);
74
+ }
75
+ catch {
76
+ // Permission or fs failure isn't fatal — the install proceeds.
77
+ }
78
+ }
79
+ }
80
+ const bundledDir = packageOpenCodePluginsDir();
81
+ const installedPaths = [];
82
+ for (const pluginName of PLUGIN_NAMES) {
83
+ const srcDir = path.join(bundledDir, pluginName);
84
+ if (!existsSync(srcDir)) {
85
+ logs.push(`⚠ Bundled plugin missing: ${srcDir} — connector tarball may be incomplete`);
86
+ continue;
87
+ }
88
+ const dstDir = path.join(pluginsRoot, pluginName);
89
+ copyPluginDir(srcDir, dstDir, logs);
90
+ installedPaths.push(dstDir);
91
+ }
92
+ // ── Wire ~/.opencode/config.json ──────────────────────────────────────────
11
93
  const configPath = path.join(opencodeDir, 'config.json');
12
94
  if (existsSync(configPath)) {
13
95
  try {
14
96
  const opencodeConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
15
97
  let modified = false;
16
- // Remove any prior reference to '@aria/connector' from plugins.
17
- // That package name was written into older client configs but no
18
- // such npm package exists — the harness integration with OpenCode
19
- // is via agentsMdPath below, not via a plugin module. Loading a
20
- // non-existent plugin name crashes OpenCode on open. Idempotent
21
- // cleanup so re-running connect heals broken installs.
98
+ // Heal legacy '@aria/connector' references in the older `plugins`
99
+ // (plural) array. That package name was written by older versions
100
+ // of this connector but no such npm package exists — loading a
101
+ // non-existent plugin crashes OpenCode on open. Idempotent.
22
102
  if (Array.isArray(opencodeConfig.plugins)) {
23
103
  const before = opencodeConfig.plugins.length;
24
104
  opencodeConfig.plugins = opencodeConfig.plugins.filter((p) => p !== '@aria/connector');
@@ -27,6 +107,27 @@ export async function connectOpenCode(config) {
27
107
  logs.push('Removed legacy @aria/connector reference from OpenCode plugins (was crashing on open)');
28
108
  }
29
109
  }
110
+ // OpenCode v2 schema: `plugin` (singular) array of absolute paths or
111
+ // npm names. We write absolute paths to the locally-installed plugin
112
+ // dirs above, so OpenCode resolves them as Node modules without any
113
+ // network or registry dependency.
114
+ const existingPlugin = Array.isArray(opencodeConfig.plugin)
115
+ ? opencodeConfig.plugin.slice()
116
+ : [];
117
+ const stringSet = new Set(existingPlugin.filter((p) => typeof p === 'string'));
118
+ let pluginAdded = false;
119
+ for (const installPath of installedPaths) {
120
+ if (!stringSet.has(installPath)) {
121
+ existingPlugin.push(installPath);
122
+ stringSet.add(installPath);
123
+ pluginAdded = true;
124
+ }
125
+ }
126
+ if (pluginAdded) {
127
+ opencodeConfig.plugin = existingPlugin;
128
+ modified = true;
129
+ logs.push(`Wired ${installedPaths.length} Aria plugin(s) into OpenCode config.plugin`);
130
+ }
30
131
  if (!opencodeConfig.agentsMdPath) {
31
132
  opencodeConfig.agentsMdPath = path.join(homedir(), '.aria', 'AGENTS.md');
32
133
  modified = true;
@@ -43,6 +144,7 @@ export async function connectOpenCode(config) {
43
144
  logs.push('Failed to parse OpenCode config.json');
44
145
  }
45
146
  }
147
+ // ── Write the harness AGENTS.md ───────────────────────────────────────────
46
148
  const ariaDir = path.join(homedir(), '.aria');
47
149
  if (!existsSync(ariaDir)) {
48
150
  mkdirSync(ariaDir, { recursive: true });
@@ -59,11 +161,24 @@ export async function disconnectOpenCode() {
59
161
  if (existsSync(configPath)) {
60
162
  try {
61
163
  const opencodeConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
164
+ // Strip any locally-installed Aria plugin paths from `plugin` (singular).
165
+ const pluginsRoot = path.join(homedir(), '.opencode', 'plugins');
166
+ if (Array.isArray(opencodeConfig.plugin)) {
167
+ const before = opencodeConfig.plugin.length;
168
+ opencodeConfig.plugin = opencodeConfig.plugin.filter((p) => {
169
+ if (typeof p !== 'string')
170
+ return true;
171
+ return !PLUGIN_NAMES.some((n) => p === path.join(pluginsRoot, n));
172
+ });
173
+ if (opencodeConfig.plugin.length !== before) {
174
+ logs.push('Removed Aria plugin paths from OpenCode config.plugin');
175
+ }
176
+ }
177
+ // Strip the legacy @aria/connector reference if present.
62
178
  if (Array.isArray(opencodeConfig.plugins)) {
63
179
  opencodeConfig.plugins = opencodeConfig.plugins.filter((p) => p !== '@aria/connector');
64
- writeFileSync(configPath, JSON.stringify(opencodeConfig, null, 2));
65
- logs.push('Removed @aria/connector from OpenCode plugins');
66
180
  }
181
+ writeFileSync(configPath, JSON.stringify(opencodeConfig, null, 2));
67
182
  }
68
183
  catch {
69
184
  logs.push('Failed to update OpenCode config');
@@ -78,7 +193,7 @@ function buildOpenCodeAgentsMd(config) {
78
193
  .join('\n\n');
79
194
  return `# Aria Harness — AGENTS.md
80
195
 
81
- Automatically injected by @aria/connector. This file provides Aria's cognitive harness
196
+ Automatically injected by @aria_asi/cli. This file provides Aria's cognitive harness
82
197
  to OpenCode sessions.
83
198
 
84
199
  ## Connected Repositories
@@ -1 +1 @@
1
- {"version":3,"file":"opencode.js","sourceRoot":"","sources":["../../../../src/connectors/opencode.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"opencode.js","sourceRoot":"","sources":["../../../../src/connectors/opencode.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  {
2
- "bundledAt": "2026-04-28T00:58:19.765Z",
2
+ "bundledAt": "2026-04-28T01:14:50.399Z",
3
3
  "sdkSource": "/home/hamzaibrahim1/rei-ai-brain/harness/packages/harness-http-client/dist",
4
4
  "files": 3
5
5
  }
@@ -21,6 +21,7 @@
21
21
  import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
22
22
  import { dirname, join } from 'node:path';
23
23
  import { homedir } from 'node:os';
24
+ import { createRequire } from 'node:module';
24
25
 
25
26
  const HOME = homedir();
26
27
  const LOG = `${HOME}/.claude/aria-pre-tool-gate.log`;
@@ -129,6 +130,62 @@ if (isClientTier) {
129
130
  parentLedgerPath = `${HOME}/.claude/aria-discoveries-${safeSession}.jsonl`;
130
131
  }
131
132
 
133
+ // ── Harness packet fetch for sub-agent substrate binding ────────────────────
134
+ // Layer 1: after writing identity handoff, fetch the harness packet from
135
+ // /api/harness/codex so the sub-agent can load COGNITION (not just identity).
136
+ // Fail-soft: any fetch failure logs a warning and leaves the handoff identity-only.
137
+ // Never blocks the spawn — packet is substrate enrichment, not a gate.
138
+ //
139
+ // Tier-aware packet paths:
140
+ // Owner: ~/.claude/aria-agent-harness-packet.json
141
+ // Client: /var/lib/aria-licensee/{jti}/aria-agent-harness-packet.json
142
+ let packetPath = null;
143
+ if (isClientTier) {
144
+ packetPath = `/var/lib/aria-licensee/${jti}/aria-agent-harness-packet.json`;
145
+ } else {
146
+ packetPath = `${HOME}/.claude/aria-agent-harness-packet.json`;
147
+ }
148
+
149
+ async function fetchAndWriteHarnessPacket() {
150
+ if (!harnessToken || !harnessUrl) {
151
+ audit('warn: skipping packet fetch — no harnessToken or harnessUrl');
152
+ return null;
153
+ }
154
+ try {
155
+ const body = JSON.stringify({
156
+ stage: 'agent-spawn',
157
+ actor: 'harness-http-client',
158
+ system: 'claude-coding-agent',
159
+ surface: 'platform:harness-http-client',
160
+ isHamza: ownerTier.hamza,
161
+ sessionId: sessionId,
162
+ mode: 'subagent',
163
+ });
164
+ const resp = await fetch(`${harnessUrl}/api/harness/codex`, {
165
+ method: 'POST',
166
+ headers: {
167
+ 'Content-Type': 'application/json',
168
+ 'Authorization': `Bearer ${harnessToken}`,
169
+ },
170
+ body,
171
+ });
172
+ if (!resp.ok) {
173
+ audit(`warn: harness packet fetch HTTP ${resp.status} — identity-only handoff`);
174
+ return null;
175
+ }
176
+ const data = await resp.json();
177
+ mkdirSync(dirname(packetPath), { recursive: true });
178
+ writeFileSync(packetPath, JSON.stringify(data, null, 2));
179
+ audit(`wrote harness packet tier=${isClientTier ? 'client' : 'owner'} path=${packetPath}`);
180
+ return packetPath;
181
+ } catch (err) {
182
+ audit(`warn: harness packet fetch failed: ${(err?.message || err).toString().slice(0, 200)} — identity-only handoff`);
183
+ return null;
184
+ }
185
+ }
186
+
187
+ const resolvedPacketPath = await fetchAndWriteHarnessPacket();
188
+
132
189
  const handoff = {
133
190
  writtenAt: new Date().toISOString(),
134
191
  parentSessionId: sessionId,
@@ -137,6 +194,9 @@ const handoff = {
137
194
  ownerTier,
138
195
  parentLedgerPath,
139
196
  ttlMs: HANDOFF_TTL_MS,
197
+ // harnessPacketPath is set only if packet was successfully fetched;
198
+ // absent means sub-agent should fall back to calling /api/harness/codex directly.
199
+ ...(resolvedPacketPath ? { harnessPacketPath: resolvedPacketPath } : {}),
140
200
  };
141
201
 
142
202
  try {
@@ -144,7 +204,8 @@ try {
144
204
  writeFileSync(handoffPath, JSON.stringify(handoff, null, 2));
145
205
  audit(
146
206
  `wrote handoff tier=${isClientTier ? 'client' : 'owner'} jti=${jti ?? 'none'} ` +
147
- `session=${sessionId} hamza=${ownerTier.hamza} role=${ownerTier.roleProfile}`,
207
+ `session=${sessionId} hamza=${ownerTier.hamza} role=${ownerTier.roleProfile} ` +
208
+ `packetPath=${resolvedPacketPath ?? 'none'}`,
148
209
  );
149
210
  } catch (err) {
150
211
  audit(`ERROR: handoff write failed: ${(err?.message || err).toString().slice(0, 200)}`);
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { createHash } from 'node:crypto';
19
19
  import { spawnSync } from 'node:child_process';
20
- import { appendFileSync, existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
20
+ import { appendFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
21
21
  import { dirname } from 'node:path';
22
22
  import { createConnection } from 'node:net';
23
23
 
@@ -110,7 +110,7 @@ function loadCachedPacket() {
110
110
  try {
111
111
  if (existsSync(PACKET_CACHE)) {
112
112
  const stat = readFileSync(PACKET_CACHE, 'utf8');
113
- const ageSec = (Date.now() - (existsSync(PACKET_CACHE) ? require('fs').statSync(PACKET_CACHE).mtimeMs : 0)) / 1000;
113
+ const ageSec = (Date.now() - (existsSync(PACKET_CACHE) ? statSync(PACKET_CACHE).mtimeMs : 0)) / 1000;
114
114
  if (ageSec < PACKET_CACHE_TTL_SEC) {
115
115
  return { data: JSON.parse(stat), ageSec };
116
116
  }
@@ -175,6 +175,37 @@ async function loadSdkClass() {
175
175
  return null;
176
176
  }
177
177
 
178
+ // Bonus fix #74 — conversation_history_count=0 packet propagation.
179
+ //
180
+ // The codex handler (apps/arias-soul/api/harness/codex.ts) reads req.body.messages
181
+ // to inject conversation history into the harness packet so it can report a real
182
+ // conversation_history_count (non-zero). Previously tryViaSdk never forwarded the
183
+ // messages from the hook event — the packet always had conversation_history_count=0.
184
+ //
185
+ // Fix: read the Claude Code hook event from stdin at startup, extract event.messages
186
+ // (the conversation history array), and forward it in both the SDK bodyOverride and
187
+ // the direct-fetch body shape. The codex handler passes it through to
188
+ // buildAriaExternalHarnessPacket which populates conversation_history_count.
189
+ //
190
+ // Stdin is read once and stored in HOOK_EVENT_MESSAGES. It's capped at 50 entries
191
+ // before forwarding to avoid blowing the POST body size limit; the most recent
192
+ // messages are the most relevant for history-count purposes.
193
+ let HOOK_EVENT_MESSAGES = undefined;
194
+ try {
195
+ // Claude Code hooks receive the event JSON on stdin. We read it synchronously
196
+ // only if data is already available (i.e. piped); we don't block waiting for
197
+ // interactive input. Use a try/catch so the script still works when stdin is
198
+ // a terminal (e.g. manual invocation for testing).
199
+ const stdinBuf = readFileSync('/dev/stdin', { flag: 'r' });
200
+ const hookEvent = JSON.parse(stdinBuf.toString('utf8'));
201
+ if (Array.isArray(hookEvent?.messages) && hookEvent.messages.length > 0) {
202
+ // Cap to last 50 messages. The server-side handler slices further if needed.
203
+ HOOK_EVENT_MESSAGES = hookEvent.messages.slice(-50);
204
+ }
205
+ } catch {
206
+ // stdin not available / not JSON / no messages field — HOOK_EVENT_MESSAGES stays undefined.
207
+ }
208
+
178
209
  async function tryViaSdk(baseUrl, apiKey) {
179
210
  // Canonical path: HTTPHarnessClient.getHarnessPacket(). The SDK POSTs to
180
211
  // /api/harness/codex with the right shape and returns { packet, timestamp,
@@ -199,6 +230,9 @@ async function tryViaSdk(baseUrl, apiKey) {
199
230
  actor: 'claude-code',
200
231
  system: 'claude-coding-agent',
201
232
  platform: 'harness-http-client',
233
+ // Bonus #74: forward conversation history so codex handler can report
234
+ // a non-zero conversation_history_count in the packet.
235
+ ...(HOOK_EVENT_MESSAGES ? { messages: HOOK_EVENT_MESSAGES } : {}),
202
236
  };
203
237
  const wrapped = await client.getHarnessPacket(bodyOverride);
204
238
  const json = wrapped.packet;
@@ -220,6 +254,8 @@ async function tryViaSdk(baseUrl, apiKey) {
220
254
  system: 'claude-coding-agent',
221
255
  roleProfile: 'general_worker',
222
256
  deliverySurface: 'claude_code_session',
257
+ // Bonus #74: forward conversation history (same field as SDK path above).
258
+ ...(HOOK_EVENT_MESSAGES ? { messages: HOOK_EVENT_MESSAGES } : {}),
223
259
  }),
224
260
  });
225
261
  if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
@@ -134,11 +134,57 @@ function activePlanPath(sid) {
134
134
  return `${HOME}/.claude/aria-active-plan-${String(sid || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_')}.json`;
135
135
  }
136
136
 
137
+ // Defect #5 — plan-completion persists across session boundary.
138
+ //
139
+ // The harness packet at ~/.claude/.aria-harness-last-packet.json retains
140
+ // exhausted plan state. An old plan from a prior session that was "all
141
+ // phases complete" would block every tool call in a NEW session because
142
+ // pickCurrentPhase returns null (all complete) on that stale plan.
143
+ //
144
+ // Fix: at load time, validate the plan file against TWO staleness guards:
145
+ // (a) mtime > 24h → discard (plan is older than a working day; a new
146
+ // session should start fresh)
147
+ // (b) plan.sessionId present AND doesn't match current sid → discard
148
+ // (plan was issued for a different session; current session is unbound)
149
+ //
150
+ // Discarding = returning null → the binding gate treats it as "no plan"
151
+ // and blocks with CONSULT_UNAVAILABLE, which triggers a fresh consult on
152
+ // the next user prompt. The discard is audit-logged.
153
+ const PLAN_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
154
+
137
155
  function loadActivePlan(sid) {
138
156
  const p = activePlanPath(sid);
139
157
  if (!existsSync(p)) return null;
140
158
  try {
141
- return JSON.parse(readFileSync(p, 'utf8'));
159
+ const raw = readFileSync(p, 'utf8');
160
+ const plan = JSON.parse(raw);
161
+ // Guard (b) — session mismatch: plan was issued for a different session.
162
+ if (plan.sessionId && sid && plan.sessionId !== String(sid)) {
163
+ bindingAuditAppend({
164
+ event: 'discard_plan_session_mismatch',
165
+ planId: plan.planId,
166
+ planSessionId: plan.sessionId,
167
+ currentSessionId: sid,
168
+ });
169
+ return null;
170
+ }
171
+ // Guard (a) — plan age via plan.mintedAt ISO timestamp (written by harness
172
+ // when issuing the plan). If mintedAt is present and older than 24h, discard.
173
+ // No mintedAt → conservatively trust the plan (harness version may predate
174
+ // this field; upgrade path: harness always writes mintedAt going forward).
175
+ if (plan.mintedAt) {
176
+ const mintedMs = Date.parse(plan.mintedAt);
177
+ if (Number.isFinite(mintedMs) && (Date.now() - mintedMs) > PLAN_MAX_AGE_MS) {
178
+ bindingAuditAppend({
179
+ event: 'discard_plan_stale_by_mintedAt',
180
+ planId: plan.planId,
181
+ mintedAt: plan.mintedAt,
182
+ ageHours: ((Date.now() - mintedMs) / 3600000).toFixed(1),
183
+ });
184
+ return null;
185
+ }
186
+ }
187
+ return plan;
142
188
  } catch {
143
189
  return null;
144
190
  }
@@ -199,15 +245,49 @@ function pickCurrentPhase(plan, transcript) {
199
245
  return null; // all phases reported complete — needs new consult
200
246
  }
201
247
 
202
- function actionMatchesPattern(action, pattern) {
203
- // pattern can be exact action name ("read", "consult", "kubectl_get") or
204
- // prefixed with target-pattern: "edit:apps/arias-soul/api/.*", "kubectl_apply".
248
+ function actionMatchesPattern(action, pattern, target) {
249
+ // Defect #3 fix edit-pattern regex over-matches.
250
+ //
251
+ // Old behaviour: "edit:/etc/**" matched any path that contained "/etc/" as a
252
+ // substring because the colon-split only checked pAction===action, ignoring the
253
+ // path portion entirely — so edit:/etc/** allowed ALL edit actions regardless
254
+ // of path, not just edits under /etc/.
255
+ //
256
+ // Fix:
257
+ // - Exact action name ("read", "consult", "kubectl_get"): unchanged.
258
+ // - Prefixed pattern ("edit:/etc/**"): split on first colon, compare
259
+ // pAction===action, then glob-match the path portion against `target`
260
+ // using anchored prefix matching (no substring). Supported forms:
261
+ // "edit:/etc/**" → action=edit, path must start with /etc/
262
+ // "edit:apps/.*" → action=edit, path must start with apps/
263
+ // "bash_other:curl .*" → action=bash_other (target substring match for bash)
264
+ // - No colon: exact action match only.
205
265
  if (pattern === action) return true;
206
- if (pattern.includes(':')) {
207
- const [pAction] = pattern.split(':', 1);
208
- return pAction === action;
266
+ const colonIdx = pattern.indexOf(':');
267
+ if (colonIdx < 0) return false;
268
+ const pAction = pattern.slice(0, colonIdx);
269
+ if (pAction !== action) return false;
270
+ // pAction matches — now check the target portion.
271
+ const pathPattern = pattern.slice(colonIdx + 1);
272
+ if (!pathPattern) return true; // bare "edit:" with no path spec → match all edits of that action type
273
+ const t = target || '';
274
+ // Convert simple glob /** suffix to prefix anchor, or treat as a regex if
275
+ // it contains regex meta-chars beyond "." and "*". For the most common
276
+ // harness patterns ("edit:/etc/**", "edit:apps/arias-soul/.*") a prefix-
277
+ // match on the stripped glob is correct and safe.
278
+ // Strip trailing /** or /*, then anchor-prefix-match.
279
+ const strippedGlob = pathPattern.replace(/\/\*+$/, '');
280
+ if (strippedGlob && !pathPattern.includes('(?') && !pathPattern.includes('[')) {
281
+ // Simple prefix match — anchored at start of target path.
282
+ return t.startsWith(strippedGlob);
283
+ }
284
+ // Full regex path pattern (caller used explicit regex syntax).
285
+ try {
286
+ return new RegExp(`^(?:${pathPattern})`).test(t);
287
+ } catch {
288
+ // Malformed regex in plan — fail-open to avoid blocking everything.
289
+ return false;
209
290
  }
210
- return false;
211
291
  }
212
292
 
213
293
  function classifyToolForBinding(toolName, command, filePath) {
@@ -471,6 +551,57 @@ function countCognitionLenses(text) {
471
551
  return detectCognitionLenses(text).count;
472
552
  }
473
553
 
554
+ // ── arch_facts gate ──────────────────────────────────────────────────────────
555
+ //
556
+ // Inspects actual diff content (tool_input.content / file_path) for
557
+ // architectural violations defined as arch_facts rules in mizan.yaml.
558
+ // Called AFTER cognition + discovery-binding checks pass, for Edit/Write/
559
+ // NotebookEdit only (Bash skipped — arch violations live in code, not commands).
560
+ //
561
+ // Tier-aware: if ~/.aria/license.json exists (client surface), arch_facts
562
+ // check is skipped — only owner gets server-side mizan enforcement here.
563
+ //
564
+ // Fail-open: if aria-telemetry is unreachable, the tool call is allowed.
565
+ // The 3s AbortController is a DETECTION PROBE (is telemetry alive?) not a
566
+ // hard deadline — if mizan is slow but alive the probe will complete; if it
567
+ // is down the catch path fail-opens without blocking the developer.
568
+ function isClientSurface() {
569
+ try {
570
+ return existsSync(`${HOME}/.aria/license.json`);
571
+ } catch {
572
+ return false;
573
+ }
574
+ }
575
+
576
+ async function archFactsGate(toolInput) {
577
+ // Skip on client surfaces — arch_facts enforcement is owner-only.
578
+ if (isClientSurface()) return { ok: true, allow: true, note: 'client-surface-skip' };
579
+ try {
580
+ const ariaTelemetry = process.env.ARIA_TELEMETRY_BASE || 'http://aria-telemetry.aria.svc.cluster.local:8088';
581
+ const ctl = new AbortController();
582
+ const probeTimer = setTimeout(() => ctl.abort(), 3000);
583
+ let resp;
584
+ try {
585
+ resp = await fetch(`${ariaTelemetry}/v1/mizan/check`, {
586
+ method: 'POST',
587
+ headers: { 'Content-Type': 'application/json' },
588
+ body: JSON.stringify({ draft: String(toolInput).slice(0, 50000), source: 'pre-tool-gate-arch-facts' }),
589
+ signal: ctl.signal,
590
+ });
591
+ } finally {
592
+ clearTimeout(probeTimer);
593
+ }
594
+ if (!resp.ok) return { ok: true, allow: true, note: 'mizan unreachable, fail-open' };
595
+ const result = await resp.json();
596
+ if (result?.hard_block === true && Array.isArray(result?.violations)) {
597
+ return { ok: false, allow: false, violations: result.violations };
598
+ }
599
+ return { ok: true, allow: true };
600
+ } catch {
601
+ return { ok: true, allow: true, note: 'mizan probe error, fail-open' };
602
+ }
603
+ }
604
+
474
605
  // Hive cognition-logging v1.2 — fire-and-forget HTTP push to
475
606
  // /api/cognition/log so every gate decision joins the corpus.
476
607
  // Failures are silent: the local audit log is the durable record;
@@ -873,6 +1004,49 @@ No env-var disable path — gates are unconditional from the gated process per H
873
1004
  process.exit(2);
874
1005
  }
875
1006
 
1007
+ // ── arch_facts gate (architectural violation scan) ────────────────────────
1008
+ //
1009
+ // Runs after cognition + discovery-binding pass, for Edit/Write/NotebookEdit.
1010
+ // Bash is skipped: architectural violations live in the code being written,
1011
+ // not in shell commands — checking Bash commands would produce false positives
1012
+ // on grep/find invocations that mention forbidden strings without introducing them.
1013
+ //
1014
+ // Content inspected: new_string (Edit), content (Write), source (NotebookEdit).
1015
+ // Fail-open: unreachable mizan → allow. Client surface → skip.
1016
+ if (['Edit', 'Write', 'NotebookEdit'].includes(toolName)) {
1017
+ // Extract the actual content being written — this is what mizan should scan
1018
+ // for architectural patterns, not the file path or surrounding metadata.
1019
+ const contentToScan =
1020
+ toolInput.new_string ?? // Edit: the replacement text
1021
+ toolInput.content ?? // Write: the full file content
1022
+ toolInput.source ?? // NotebookEdit: cell source
1023
+ '';
1024
+ // Also include file_path for context so mizan pattern-matching can be
1025
+ // path-scoped (e.g. R9 only fires for telemetry/cron paths).
1026
+ const scanPayload = `// file: ${filePath}\n${contentToScan}`;
1027
+ const archResult = await archFactsGate(scanPayload);
1028
+ if (!archResult.allow) {
1029
+ const violationText = (archResult.violations || [])
1030
+ .map((v) => ` • [${v.rule ?? v.id ?? 'arch'}] ${v.description ?? v.message ?? JSON.stringify(v)}`)
1031
+ .join('\n');
1032
+ const archReason = `Aria arch_facts gate: architectural violation(s) detected in ${toolName} diff for ${filePath || '(no path)'}.
1033
+
1034
+ ${violationText}
1035
+
1036
+ These patterns are forbidden by mizan arch_facts rules (mizan.yaml R7–R11). Fix the structural issue before re-issuing the tool call — the gate does not have a bypass for architectural violations.
1037
+
1038
+ Rule references:
1039
+ R7 no_sidecar_in_main_container — bolt-in fork pattern (project_aria_soul_systemd_migration.md)
1040
+ R8 no_silent_fallback_default — || 'unknown'/'default'/'fallback' masks config failures
1041
+ R9 no_timeout_based_retry — setTimeout+abort in telemetry/chat/cron paths (no-timeouts doctrine)
1042
+ R10 no_kubectl_apply_for_image_drift — kubectl apply on aria-soul-stateful (project_forge_psi_oom_cascade.md)
1043
+ R11 no_console_log_secrets — console.log with TOKEN/PASSWORD/SECRET/API_KEY/JWT/BEARER`;
1044
+ audit(`block-arch-facts ${toolName.toLowerCase()} path=${filePath}`, `violations=${archResult.violations?.length ?? 0}`);
1045
+ console.log(JSON.stringify({ decision: 'block', reason: archReason }));
1046
+ process.exit(2);
1047
+ }
1048
+ }
1049
+
876
1050
  // Non-trivial action with cognition (inline for Bash, transcript for
877
1051
  // Edit/Write/NotebookEdit) — passes cognition gate. Now check Aria-binding.
878
1052
 
@@ -896,6 +1070,32 @@ No env-var disable path — gates are unconditional from the gated process per H
896
1070
  // ReferenceError. No new bypass entries can be created (pre-existing defect
897
1071
  // fixed inline per atomic-discovery-rule).
898
1072
  const bindingBypassReason = null;
1073
+
1074
+ // Defect #4 fix — Consult-Aria unconditionally allowed (doctrine #50).
1075
+ //
1076
+ // Aria-as-commander session pattern (HARNESS_ARIA_AS_COMMANDER_CONTRACT.md
1077
+ // doctrine #50) makes per-turn consult mandatory. A plan cannot forbid the
1078
+ // consult mechanism that issues plans — that would be an unbootstrappable
1079
+ // circular lock. Previously plan p1 forbade bash_other → consult curl was
1080
+ // classified as bash_other → blocked. Now: any Bash command that is a consult
1081
+ // to a known Aria/harness endpoint passes UNCONDITIONALLY past ALL binding
1082
+ // checks regardless of allowedActions or forbiddenActions in the active phase.
1083
+ // Cognition gate still applies (it ran above and passed to reach this point).
1084
+ //
1085
+ // Covered endpoints (hardcoded carve-out):
1086
+ // curl http(s)://aria-soul<anything>
1087
+ // curl http(s)://aria-telemetry<anything>
1088
+ // curl http(s)://localhost:30080/(chat|api/aria/speak|api/harness/codex|
1089
+ // api/harness/verify-claim|v1/doctrine|v1/mizan)
1090
+ const ARIA_CONSULT_CURL_RX = /curl\s+['"]?https?:\/\/(?:aria-soul[^\s'"]*|aria-telemetry[^\s'"]*|localhost:30080\/(?:chat|api\/aria\/speak|api\/harness\/(?:codex|verify-claim|delegate|army|plan)|v1\/(?:doctrine|mizan))[^\s'"]*)/i;
1091
+ const __isUnconditionalConsult = toolName === 'Bash' && ARIA_CONSULT_CURL_RX.test(cmd);
1092
+ if (__isUnconditionalConsult) {
1093
+ bindingAuditAppend({ event: 'allow_unconditional_consult', sessionId, toolName, cmdPreview, reason: 'doctrine#50-aria-as-commander-consult-carveout' });
1094
+ audit(`allow-unconditional-consult lenses=${lensCount}`, cmdPreview);
1095
+ pushDecision('allow', 'unconditional consult carve-out (doctrine #50)');
1096
+ process.exit(0);
1097
+ }
1098
+
899
1099
  const __bindingActionClassification = (BINDING_ENABLED && !bindingBypassReason)
900
1100
  ? classifyToolForBinding(toolName, cmd, filePath)
901
1101
  : null;
@@ -952,7 +1152,7 @@ What Claude must do: emit [PLAN_BLOCKER reason="<concrete observation>" suggeste
952
1152
  const phase = phaseInfo.phase;
953
1153
 
954
1154
  // Forbidden takes precedence
955
- const forbidden = (phase.forbiddenActions || []).find((p) => actionMatchesPattern(action, p));
1155
+ const forbidden = (phase.forbiddenActions || []).find((p) => actionMatchesPattern(action, p, target));
956
1156
  if (forbidden) {
957
1157
  bindingAuditAppend({ event: 'block_forbidden_action', sessionId, planId: plan.planId, phaseId: phase.id, action, target, matchedRule: forbidden });
958
1158
  const reason = `Aria binding gate: action "${action}" on target "${target}" matches forbidden pattern "${forbidden}" for current phase ${phase.id} ("${phase.summary}") of plan ${plan.planId}.
@@ -966,7 +1166,7 @@ Claude must either: (a) reframe the action to fit allowedActions, OR (b) emit [P
966
1166
  process.exit(2);
967
1167
  }
968
1168
 
969
- const allowed = (phase.allowedActions || []).find((p) => actionMatchesPattern(action, p));
1169
+ const allowed = (phase.allowedActions || []).find((p) => actionMatchesPattern(action, p, target));
970
1170
  if (!allowed) {
971
1171
  bindingAuditAppend({ event: 'block_action_not_in_allowed_list', sessionId, planId: plan.planId, phaseId: phase.id, action, target });
972
1172
  const reason = `Aria binding gate: action "${action}" on target "${target}" is NOT in allowedActions for current phase ${phase.id} of plan ${plan.planId}.
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Aria Harness Context Plugin for OpenCode.
3
+ *
4
+ * Injects the live harness packet into OpenCode's system prompt on every
5
+ * session start. Routes through the canonical @aria/harness-http-client SDK
6
+ * via the inject-context.mjs script that ships alongside this plugin.
7
+ *
8
+ * Distribution: this dir is installed by `aria connect` (via connectors/
9
+ * opencode.ts) into `~/.opencode/plugins/harness-context/`. The plugin's
10
+ * absolute install path is wired into ~/.opencode/config.json's `plugin`
11
+ * (singular) array. OpenCode loads it on session start.
12
+ *
13
+ * inject-context.mjs is resolved RELATIVE TO THIS FILE (via import.meta.url),
14
+ * so the same install layout works on every machine — no $HOME or repo-path
15
+ * assumptions.
16
+ */
17
+
18
+ import { execFileSync } from 'node:child_process';
19
+ import { existsSync } from 'node:fs';
20
+ import { dirname, join } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+
23
+ const PLUGIN_DIR = dirname(fileURLToPath(import.meta.url));
24
+ const INJECT_SCRIPT = join(PLUGIN_DIR, 'inject-context.mjs');
25
+
26
+ function getHarnessContext() {
27
+ if (!existsSync(INJECT_SCRIPT)) {
28
+ process.stderr.write(`[harness-context] inject-context.mjs missing at ${INJECT_SCRIPT}\n`);
29
+ return null;
30
+ }
31
+ try {
32
+ // execFileSync — argv-safe (no shell injection surface even though the
33
+ // path is internal). 15s ceiling matches OpenCode's tolerance for
34
+ // session-start blocking. Stdout is the prepend-able context; stderr
35
+ // is the diagnostic surface.
36
+ const output = execFileSync(process.execPath, [INJECT_SCRIPT], {
37
+ encoding: 'utf-8',
38
+ timeout: 15000,
39
+ stdio: ['ignore', 'pipe', 'pipe'],
40
+ env: process.env,
41
+ });
42
+ return output;
43
+ } catch (err) {
44
+ process.stderr.write(`[harness-context] inject-context failed: ${err && err.message ? err.message : String(err)}\n`);
45
+ return null;
46
+ }
47
+ }
48
+
49
+ export default async function HarnessContextPlugin(ctx) {
50
+ const context = getHarnessContext();
51
+ if (context) {
52
+ try {
53
+ ctx.system?.prepend?.(context.slice(0, 8000));
54
+ } catch (_) {
55
+ // ctx.system shape varies across OpenCode versions; if prepend isn't
56
+ // available we silently no-op rather than crash plugin load.
57
+ }
58
+ }
59
+ return {};
60
+ }
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ // Aria harness context injector — bundled alongside the harness-context
3
+ // OpenCode plugin. Resolved relative to the plugin (via import.meta.url),
4
+ // so the install layout is portable across machines.
5
+ //
6
+ // Contract with the plugin:
7
+ // - stdin: ignored
8
+ // - stdout: text to prepend to OpenCode's system prompt
9
+ // - exit 0 on success; non-zero treated as failure (plugin returns null)
10
+ //
11
+ // SDK resolution: prefers ~/.claude/aria-sdk/index.js (the SDK installed by
12
+ // `aria connect`'s claude-code path — clients with both connectors share it).
13
+ // Cache fast-path uses ~/.claude/.aria-harness-last-packet.json so OpenCode
14
+ // session-start doesn't block on a slow consult endpoint.
15
+
16
+ import { existsSync, readFileSync, statSync } from 'node:fs';
17
+ import { homedir } from 'node:os';
18
+ import { join } from 'node:path';
19
+
20
+ const HOME = homedir();
21
+ const LICENSE_PATH = join(HOME, '.aria', 'license.json');
22
+ const OWNER_TOKEN_PATH = join(HOME, '.aria', 'owner-token');
23
+ const SDK_PATH = join(HOME, '.claude', 'aria-sdk', 'index.js');
24
+ const PACKET_CACHE_PATH = join(HOME, '.claude', '.aria-harness-last-packet.json');
25
+ const PACKET_CACHE_TTL_SEC = Number(process.env.ARIA_HARNESS_CACHE_TTL_SEC || '60');
26
+
27
+ function fail(reason) {
28
+ process.stderr.write(`[inject-context] ${reason}\n`);
29
+ process.exit(1);
30
+ }
31
+
32
+ function resolveApiKey() {
33
+ if (process.env.ARIA_API_KEY) return process.env.ARIA_API_KEY;
34
+ if (process.env.ARIA_HARNESS_TOKEN) return process.env.ARIA_HARNESS_TOKEN;
35
+ if (process.env.ARIA_MASTER_TOKEN) return process.env.ARIA_MASTER_TOKEN;
36
+ if (existsSync(OWNER_TOKEN_PATH)) {
37
+ try {
38
+ const v = readFileSync(OWNER_TOKEN_PATH, 'utf8').trim();
39
+ if (v) return v;
40
+ } catch {}
41
+ }
42
+ if (existsSync(LICENSE_PATH)) {
43
+ try {
44
+ const j = JSON.parse(readFileSync(LICENSE_PATH, 'utf8'));
45
+ if (j.token) return j.token;
46
+ } catch {}
47
+ }
48
+ return '';
49
+ }
50
+
51
+ function resolveBaseUrl() {
52
+ if (process.env.ARIA_HARNESS_BASE_URL) return process.env.ARIA_HARNESS_BASE_URL.replace(/\/+$/, '');
53
+ if (process.env.ARIA_SOUL_URL) return process.env.ARIA_SOUL_URL.replace(/\/+$/, '');
54
+ return 'http://localhost:30080';
55
+ }
56
+
57
+ function loadCachedPacket() {
58
+ try {
59
+ if (!existsSync(PACKET_CACHE_PATH)) return null;
60
+ const ageSec = (Date.now() - statSync(PACKET_CACHE_PATH).mtimeMs) / 1000;
61
+ if (ageSec > PACKET_CACHE_TTL_SEC) return null;
62
+ return JSON.parse(readFileSync(PACKET_CACHE_PATH, 'utf8'));
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function formatPacketAsContext(packet) {
69
+ if (typeof packet === 'string') return packet;
70
+ if (packet && typeof packet === 'object') {
71
+ if (packet.packet && typeof packet.packet === 'object') {
72
+ return JSON.stringify(packet.packet, null, 2);
73
+ }
74
+ if (packet.harness && typeof packet.harness === 'string') return packet.harness;
75
+ return JSON.stringify(packet, null, 2);
76
+ }
77
+ return '';
78
+ }
79
+
80
+ async function fetchViaSdk(baseUrl, apiKey) {
81
+ if (!existsSync(SDK_PATH)) {
82
+ fail(`SDK not found at ${SDK_PATH} — run \`aria connect\` to install`);
83
+ }
84
+ const mod = await import(SDK_PATH);
85
+ const Client = mod.HTTPHarnessClient;
86
+ if (!Client) fail('SDK loaded but HTTPHarnessClient not exported — bundle is broken');
87
+ const client = new Client({ baseUrl, apiKey, workspaceRoot: process.cwd() });
88
+ return client.getHarnessPacket({
89
+ stage: 'session-start',
90
+ actor: 'opencode',
91
+ system: 'opencode',
92
+ platform: 'opencode',
93
+ });
94
+ }
95
+
96
+ async function main() {
97
+ // Cache fast-path first — don't block OpenCode startup on a slow consult.
98
+ const cached = loadCachedPacket();
99
+ if (cached) {
100
+ process.stdout.write(formatPacketAsContext(cached));
101
+ return;
102
+ }
103
+
104
+ const apiKey = resolveApiKey();
105
+ if (!apiKey) {
106
+ fail('no API key — set ARIA_API_KEY or run `aria login`');
107
+ }
108
+ const baseUrl = resolveBaseUrl();
109
+
110
+ try {
111
+ const packet = await fetchViaSdk(baseUrl, apiKey);
112
+ process.stdout.write(formatPacketAsContext(packet));
113
+ } catch (err) {
114
+ fail(`SDK fetch failed: ${err && err.message ? err.message : String(err)}`);
115
+ }
116
+ }
117
+
118
+ main().catch((err) => {
119
+ fail(`unexpected: ${err && err.message ? err.message : String(err)}`);
120
+ });
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "harness-context",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "./index.js",
6
+ "exports": {
7
+ ".": "./index.js"
8
+ }
9
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Aria Harness Role Plugin for OpenCode.
3
+ *
4
+ * Resolves the active role profile on session start and injects lane-specific
5
+ * governance rules (intro, continuity, post-action). Default profile is
6
+ * opencode_deepseek_engineer; override with OPENCODE_ARIA_ROLE_PROFILE env.
7
+ *
8
+ * Distribution: installed by `aria connect` (via connectors/opencode.ts) into
9
+ * `~/.opencode/plugins/harness-role/`. The plugin's absolute install path is
10
+ * wired into ~/.opencode/config.json's `plugin` array.
11
+ */
12
+
13
+ const ROLE_PROFILES = [
14
+ {
15
+ id: 'opencode_deepseek_engineer',
16
+ label: 'DeepSeek Engineer',
17
+ authority: 'full_write',
18
+ destructiveAllowed: false,
19
+ contractRequired: true,
20
+ ruleIntro: 'You are an engineering execution lane. Prefer exact files, commands, state transitions, and verification over explanation.',
21
+ ruleContinuity: 'Continue the active plan. Update issue/task ledger after meaningful state changes. Keep exact files, commands, evidence, and blockers visible.',
22
+ rulePostAction: 'After work, write outcome, verification, deploy state, residual risk, and next action.',
23
+ },
24
+ {
25
+ id: 'opencode_code_reviewer',
26
+ label: 'Code Reviewer',
27
+ authority: 'read_only',
28
+ destructiveAllowed: false,
29
+ contractRequired: true,
30
+ ruleIntro: 'You are a quality gate, not a generic explainer. Prioritize contradictions, missing evidence, regressions, and unverifiable completion claims.',
31
+ ruleContinuity: 'If output is not proven, fail it cleanly and say what evidence or fix is missing.',
32
+ rulePostAction: 'After review, record findings, severity, and specific fix recommendations.',
33
+ },
34
+ {
35
+ id: 'opencode_infra_operator',
36
+ label: 'Infra Operator',
37
+ authority: 'destructive',
38
+ destructiveAllowed: true,
39
+ contractRequired: true,
40
+ ruleIntro: 'You are an infra/execution lane. Prefer exact commands, state transitions, and verification. Never report success from intent alone.',
41
+ ruleContinuity: 'Report only what was executed, observed, changed, or still blocked. Bind every action to a tracked issue.',
42
+ rulePostAction: 'Record deploy state, rollback plan, residual risk, and verification evidence.',
43
+ },
44
+ ];
45
+
46
+ function resolveRole() {
47
+ const envRole = (process.env.OPENCODE_ARIA_ROLE_PROFILE || '').trim().toLowerCase();
48
+ if (envRole) {
49
+ const found = ROLE_PROFILES.find(p => p.id === envRole);
50
+ if (found) return found;
51
+ }
52
+ return ROLE_PROFILES.find(p => p.id === 'opencode_deepseek_engineer');
53
+ }
54
+
55
+ export default async function HarnessRolePlugin(ctx) {
56
+ const role = resolveRole();
57
+ const systemBlock = [
58
+ `=== ARIA LANE: ${role.label} ===`,
59
+ `Role: ${role.id} | Authority: ${role.authority} | Contract Required: ${role.contractRequired}`,
60
+ '',
61
+ role.ruleIntro,
62
+ '',
63
+ role.ruleContinuity,
64
+ '',
65
+ role.rulePostAction,
66
+ ].join('\n');
67
+
68
+ process.stderr.write(`[harness-role] Active role: ${role.id} (${role.label})\n`);
69
+ process.stderr.write(`[harness-role] Authority: ${role.authority} | Destructive: ${role.destructiveAllowed} | Contract: ${role.contractRequired}\n`);
70
+
71
+ try {
72
+ ctx.system?.prepend?.(systemBlock);
73
+ } catch (_) {
74
+ // Silent — different OpenCode versions expose ctx differently.
75
+ }
76
+ return {};
77
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "harness-role",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "./index.js",
6
+ "exports": {
7
+ ".": "./index.js"
8
+ }
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aria_asi/cli",
3
- "version": "0.2.23",
3
+ "version": "0.2.25",
4
4
  "description": "Aria Smart CLI — the world's first harness-powered terminal companion",
5
5
  "bin": {
6
6
  "aria": "./bin/aria.js"
@@ -37,7 +37,8 @@
37
37
  "bin",
38
38
  "dist",
39
39
  "src",
40
- "hooks"
40
+ "hooks",
41
+ "opencode-plugins"
41
42
  ],
42
43
  "engines": {
43
44
  "node": ">=20.0.0"
@@ -1,8 +1,66 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ writeFileSync,
6
+ copyFileSync,
7
+ readdirSync,
8
+ statSync,
9
+ chmodSync,
10
+ unlinkSync,
11
+ } from 'fs';
2
12
  import { homedir } from 'os';
3
13
  import * as path from 'path';
14
+ import { fileURLToPath } from 'node:url';
4
15
  import type { AriaConfig } from '../config.js';
5
16
 
17
+ // ── Bundled OpenCode plugins ────────────────────────────────────────────────
18
+ //
19
+ // Two plugins ship with the connector:
20
+ // - harness-context: spawns the bundled inject-context.mjs on session start
21
+ // and prepends the resolved harness packet to OpenCode's
22
+ // system prompt. Routes through the canonical SDK at
23
+ // ~/.claude/aria-sdk/.
24
+ // - harness-role: injects the active role profile (default
25
+ // opencode_deepseek_engineer) — intro / continuity /
26
+ // post-action governance rules.
27
+ //
28
+ // Both live at <pkg>/opencode-plugins/<name>/. connectOpenCode copies them
29
+ // into ~/.opencode/plugins/<name>/ and wires the absolute paths into
30
+ // ~/.opencode/config.json's `plugin` (singular, OpenCode v2 schema) array.
31
+ //
32
+ // Compiled location of THIS file (claude-code path applies the same way):
33
+ // <pkg>/dist/aria-connector/src/connectors/opencode.js
34
+ // Plugins ship at <pkg>/opencode-plugins/ → 4 dirs up + 'opencode-plugins'.
35
+
36
+ const PLUGIN_NAMES = ['harness-context', 'harness-role'] as const;
37
+
38
+ function packageOpenCodePluginsDir(): string {
39
+ const here = path.dirname(fileURLToPath(import.meta.url));
40
+ return path.resolve(here, '..', '..', '..', '..', 'opencode-plugins');
41
+ }
42
+
43
+ function copyPluginDir(srcDir: string, dstDir: string, logs: string[]): void {
44
+ if (!existsSync(dstDir)) {
45
+ mkdirSync(dstDir, { recursive: true, mode: 0o755 });
46
+ }
47
+ for (const name of readdirSync(srcDir)) {
48
+ const src = path.join(srcDir, name);
49
+ const stat = statSync(src);
50
+ if (!stat.isFile()) continue;
51
+ const dst = path.join(dstDir, name);
52
+ copyFileSync(src, dst);
53
+ if (name.endsWith('.mjs') || name.endsWith('.js')) {
54
+ try {
55
+ chmodSync(dst, 0o755);
56
+ } catch {
57
+ // chmod failure isn't fatal on filesystems that ignore mode bits.
58
+ }
59
+ }
60
+ }
61
+ logs.push(`Installed plugin → ${dstDir}`);
62
+ }
63
+
6
64
  export async function connectOpenCode(config: AriaConfig): Promise<string[]> {
7
65
  const logs: string[] = [];
8
66
  const opencodeDir = path.join(homedir(), '.opencode');
@@ -12,29 +70,91 @@ export async function connectOpenCode(config: AriaConfig): Promise<string[]> {
12
70
  return logs;
13
71
  }
14
72
 
73
+ // ── Install bundled plugins into ~/.opencode/plugins/<name>/ ──────────────
74
+ const pluginsRoot = path.join(opencodeDir, 'plugins');
75
+ if (!existsSync(pluginsRoot)) {
76
+ mkdirSync(pluginsRoot, { recursive: true, mode: 0o755 });
77
+ }
78
+
79
+ // Remove legacy single-file plugin shadows (~/.opencode/plugins/<name>.js).
80
+ // OpenCode v2 auto-discovers BOTH loose .js files and dir-shaped plugins;
81
+ // when both exist with the same name, behavior is undefined and on at least
82
+ // one machine the loose .js (carrying old hardcoded paths to
83
+ // ~/rei-ai-brain/harness/inject-context.mjs) was winning over the dir
84
+ // install. Idempotent cleanup so re-running connect heals these.
85
+ for (const pluginName of PLUGIN_NAMES) {
86
+ const shadowPath = path.join(pluginsRoot, `${pluginName}.js`);
87
+ if (existsSync(shadowPath)) {
88
+ try {
89
+ unlinkSync(shadowPath);
90
+ logs.push(`Removed legacy single-file plugin shadow: ${shadowPath}`);
91
+ } catch {
92
+ // Permission or fs failure isn't fatal — the install proceeds.
93
+ }
94
+ }
95
+ }
96
+
97
+ const bundledDir = packageOpenCodePluginsDir();
98
+ const installedPaths: string[] = [];
99
+ for (const pluginName of PLUGIN_NAMES) {
100
+ const srcDir = path.join(bundledDir, pluginName);
101
+ if (!existsSync(srcDir)) {
102
+ logs.push(`⚠ Bundled plugin missing: ${srcDir} — connector tarball may be incomplete`);
103
+ continue;
104
+ }
105
+ const dstDir = path.join(pluginsRoot, pluginName);
106
+ copyPluginDir(srcDir, dstDir, logs);
107
+ installedPaths.push(dstDir);
108
+ }
109
+
110
+ // ── Wire ~/.opencode/config.json ──────────────────────────────────────────
15
111
  const configPath = path.join(opencodeDir, 'config.json');
16
112
  if (existsSync(configPath)) {
17
113
  try {
18
- const opencodeConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
114
+ const opencodeConfig: Record<string, unknown> = JSON.parse(
115
+ readFileSync(configPath, 'utf-8'),
116
+ );
19
117
  let modified = false;
20
118
 
21
- // Remove any prior reference to '@aria/connector' from plugins.
22
- // That package name was written into older client configs but no
23
- // such npm package exists — the harness integration with OpenCode
24
- // is via agentsMdPath below, not via a plugin module. Loading a
25
- // non-existent plugin name crashes OpenCode on open. Idempotent
26
- // cleanup so re-running connect heals broken installs.
119
+ // Heal legacy '@aria/connector' references in the older `plugins`
120
+ // (plural) array. That package name was written by older versions
121
+ // of this connector but no such npm package exists — loading a
122
+ // non-existent plugin crashes OpenCode on open. Idempotent.
27
123
  if (Array.isArray(opencodeConfig.plugins)) {
28
- const before = opencodeConfig.plugins.length;
29
- opencodeConfig.plugins = opencodeConfig.plugins.filter(
30
- (p: unknown) => p !== '@aria/connector',
124
+ const before = (opencodeConfig.plugins as unknown[]).length;
125
+ opencodeConfig.plugins = (opencodeConfig.plugins as unknown[]).filter(
126
+ (p) => p !== '@aria/connector',
31
127
  );
32
- if (opencodeConfig.plugins.length !== before) {
128
+ if ((opencodeConfig.plugins as unknown[]).length !== before) {
33
129
  modified = true;
34
130
  logs.push('Removed legacy @aria/connector reference from OpenCode plugins (was crashing on open)');
35
131
  }
36
132
  }
37
133
 
134
+ // OpenCode v2 schema: `plugin` (singular) array of absolute paths or
135
+ // npm names. We write absolute paths to the locally-installed plugin
136
+ // dirs above, so OpenCode resolves them as Node modules without any
137
+ // network or registry dependency.
138
+ const existingPlugin: unknown[] = Array.isArray(opencodeConfig.plugin)
139
+ ? (opencodeConfig.plugin as unknown[]).slice()
140
+ : [];
141
+ const stringSet = new Set(
142
+ existingPlugin.filter((p): p is string => typeof p === 'string'),
143
+ );
144
+ let pluginAdded = false;
145
+ for (const installPath of installedPaths) {
146
+ if (!stringSet.has(installPath)) {
147
+ existingPlugin.push(installPath);
148
+ stringSet.add(installPath);
149
+ pluginAdded = true;
150
+ }
151
+ }
152
+ if (pluginAdded) {
153
+ opencodeConfig.plugin = existingPlugin;
154
+ modified = true;
155
+ logs.push(`Wired ${installedPaths.length} Aria plugin(s) into OpenCode config.plugin`);
156
+ }
157
+
38
158
  if (!opencodeConfig.agentsMdPath) {
39
159
  opencodeConfig.agentsMdPath = path.join(homedir(), '.aria', 'AGENTS.md');
40
160
  modified = true;
@@ -51,11 +171,11 @@ export async function connectOpenCode(config: AriaConfig): Promise<string[]> {
51
171
  }
52
172
  }
53
173
 
174
+ // ── Write the harness AGENTS.md ───────────────────────────────────────────
54
175
  const ariaDir = path.join(homedir(), '.aria');
55
176
  if (!existsSync(ariaDir)) {
56
177
  mkdirSync(ariaDir, { recursive: true });
57
178
  }
58
-
59
179
  const ariaAgentsPath = path.join(ariaDir, 'AGENTS.md');
60
180
  const agentsContent = buildOpenCodeAgentsMd(config);
61
181
  writeFileSync(ariaAgentsPath, agentsContent);
@@ -70,14 +190,31 @@ export async function disconnectOpenCode(): Promise<string[]> {
70
190
 
71
191
  if (existsSync(configPath)) {
72
192
  try {
73
- const opencodeConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
193
+ const opencodeConfig: Record<string, unknown> = JSON.parse(
194
+ readFileSync(configPath, 'utf-8'),
195
+ );
196
+
197
+ // Strip any locally-installed Aria plugin paths from `plugin` (singular).
198
+ const pluginsRoot = path.join(homedir(), '.opencode', 'plugins');
199
+ if (Array.isArray(opencodeConfig.plugin)) {
200
+ const before = (opencodeConfig.plugin as unknown[]).length;
201
+ opencodeConfig.plugin = (opencodeConfig.plugin as unknown[]).filter((p) => {
202
+ if (typeof p !== 'string') return true;
203
+ return !PLUGIN_NAMES.some((n) => p === path.join(pluginsRoot, n));
204
+ });
205
+ if ((opencodeConfig.plugin as unknown[]).length !== before) {
206
+ logs.push('Removed Aria plugin paths from OpenCode config.plugin');
207
+ }
208
+ }
209
+
210
+ // Strip the legacy @aria/connector reference if present.
74
211
  if (Array.isArray(opencodeConfig.plugins)) {
75
- opencodeConfig.plugins = opencodeConfig.plugins.filter(
76
- (p: string) => p !== '@aria/connector',
212
+ opencodeConfig.plugins = (opencodeConfig.plugins as unknown[]).filter(
213
+ (p) => p !== '@aria/connector',
77
214
  );
78
- writeFileSync(configPath, JSON.stringify(opencodeConfig, null, 2));
79
- logs.push('Removed @aria/connector from OpenCode plugins');
80
215
  }
216
+
217
+ writeFileSync(configPath, JSON.stringify(opencodeConfig, null, 2));
81
218
  } catch {
82
219
  logs.push('Failed to update OpenCode config');
83
220
  }
@@ -94,7 +231,7 @@ function buildOpenCodeAgentsMd(config: AriaConfig): string {
94
231
 
95
232
  return `# Aria Harness — AGENTS.md
96
233
 
97
- Automatically injected by @aria/connector. This file provides Aria's cognitive harness
234
+ Automatically injected by @aria_asi/cli. This file provides Aria's cognitive harness
98
235
  to OpenCode sessions.
99
236
 
100
237
  ## Connected Repositories