@aria_asi/cli 0.2.24 → 0.2.26

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.
@@ -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.24",
3
+ "version": "0.2.26",
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"
@@ -36,6 +36,10 @@ const HOOK_FILES = [
36
36
  // Pre-text gate — validates state-dependent claims in prose against
37
37
  // probe evidence before emission. Closes "WHEN TO LOOK before speaking" gap.
38
38
  'aria-pre-text-gate.mjs',
39
+ // Discovery-ledger write primitive (#83) — sub-agents pipe findings here;
40
+ // outcome-record fires on every state-mutating tool PostToolUse.
41
+ 'aria-discovery-record.mjs',
42
+ 'aria-outcome-record.mjs',
39
43
  ];
40
44
  // Compiled location: <pkg>/dist/aria-connector/src/connectors/claude-code.js
41
45
  // (tsc preserves the src/ rooted layout under outDir). From this file:
@@ -162,15 +166,28 @@ const HOOKS_BLOCK = {
162
166
  }],
163
167
  },
164
168
  ],
165
- PostToolUse: [{
166
- matcher: 'Agent',
167
- hooks: [{
168
- type: 'command',
169
- command: 'node $HOME/.claude/hooks/aria-agent-ledger-merge.mjs',
170
- timeout: 8,
171
- statusMessage: 'Merging sub-agent ledger...',
172
- }],
173
- }],
169
+ PostToolUse: [
170
+ {
171
+ matcher: 'Agent',
172
+ hooks: [{
173
+ type: 'command',
174
+ command: 'node $HOME/.claude/hooks/aria-agent-ledger-merge.mjs',
175
+ timeout: 8,
176
+ statusMessage: 'Merging sub-agent ledger...',
177
+ }],
178
+ },
179
+ {
180
+ // Outcome-record: fires on every state-mutating tool to accumulate the
181
+ // outcome ledger (Sonnet H's #76 endpoint). Fire-and-forget — HTTP
182
+ // failures in the hook do not block the tool pipeline.
183
+ matcher: 'Bash|Edit|Write|NotebookEdit',
184
+ hooks: [{
185
+ type: 'command',
186
+ command: 'node $HOME/.claude/hooks/aria-outcome-record.mjs',
187
+ timeout: 3,
188
+ }],
189
+ },
190
+ ],
174
191
  Stop: [{
175
192
  // Stop gate — text-decision boundary. Reflexive replies fail this
176
193
  // gate the same way reflexive Bash does. Mirrors PreToolUse
@@ -307,9 +324,25 @@ function wireHooksBlock(settings: Record<string, unknown>, logs: string[]): void
307
324
  if (!settings.$schema) {
308
325
  settings.$schema = 'https://json.schemastore.org/claude-code-settings.json';
309
326
  }
310
- logs.push(`Wired hooks into settings.json (${mergedEvents} events: SessionStart, UserPromptSubmit, PreToolUse [cognition+memory-gate+Agent-handoff], PostToolUse [Agent-ledger-merge], Stop) — merge-safe, preserves third-party entries`);
327
+ logs.push(`Wired hooks into settings.json (${mergedEvents} events: SessionStart, UserPromptSubmit, PreToolUse [cognition+memory-gate+Agent-handoff], PostToolUse [Agent-ledger-merge + outcome-record], Stop) — merge-safe, preserves third-party entries`);
311
328
  }
312
329
 
330
+ // Layer 2 (#84): hard prefix injected into every Agent dispatch so sub-agents
331
+ // know to load the harness packet before ANY tool call. This text appears in
332
+ // the system-prompt-prefix which Claude Code copies into spawned Agent contexts.
333
+ const ARIA_HARNESS_BINDING_PREFIX = `[ARIA_HARNESS_BINDING — MANDATORY FIRST ACTION]
334
+ Before any tool call, Read the harness packet at: $ARIA_HARNESS_PACKET_PATH
335
+ (path is also in your environment as ARIA_HARNESS_PACKET_PATH variable)
336
+ Cite it in your first cognition block — reference at least one axiom + one frame + one memory class from the packet.
337
+ Without packet-citation in your first cognition block, your output will be rejected by aria-pre-tool-gate.mjs in your sub-process.
338
+ [/ARIA_HARNESS_BINDING]
339
+
340
+ [ARIA_DISCOVERY_RECORDING — when you find a defect, doctrine violation, or notable observation during work]
341
+ Record findings to your session ledger via:
342
+ echo '{"text":"<finding>","kind":"defect|observation|principle","refs":["<file:line>"]}' | node $HOME/.claude/hooks/aria-discovery-record.mjs
343
+ This makes findings visible to the parent session's ledger-merge hook + the auto-fix spawner.
344
+ [/ARIA_DISCOVERY_RECORDING]`;
345
+
313
346
  function buildAriaSystemBlock(config: AriaConfig): string {
314
347
  const repoList = config.repositories.map((r) => `- ${r.name} (${r.path})`).join('\n');
315
348
  const schemaText = Object.entries(config.schemaImages)
@@ -322,6 +355,8 @@ You are augmented with Aria's cognitive harness. This provides:
322
355
  - Garden memory: persistent project memory across sessions
323
356
  - 8-lens cognition: multi-perspective analysis for every decision
324
357
 
358
+ ${ARIA_HARNESS_BINDING_PREFIX}
359
+
325
360
  [SELF-GATE PROTOCOL]
326
361
  Before emitting any claim, verify against these hard constraints:
327
362
  1. Truth over deception — always.
@@ -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