@aria_asi/cli 0.2.32 → 0.2.34

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.
Files changed (93) hide show
  1. package/dist/aria-connector/src/connectors/codebase-awareness.d.ts +8 -1
  2. package/dist/aria-connector/src/connectors/codebase-awareness.d.ts.map +1 -1
  3. package/dist/aria-connector/src/connectors/codebase-awareness.js +126 -71
  4. package/dist/aria-connector/src/connectors/codebase-awareness.js.map +1 -1
  5. package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -1
  6. package/dist/aria-connector/src/connectors/codex.js +98 -0
  7. package/dist/aria-connector/src/connectors/codex.js.map +1 -1
  8. package/dist/aria-connector/src/setup-wizard.d.ts.map +1 -1
  9. package/dist/aria-connector/src/setup-wizard.js +91 -24
  10. package/dist/aria-connector/src/setup-wizard.js.map +1 -1
  11. package/dist/assets/hooks/aria-harness-via-sdk.mjs +26 -8
  12. package/dist/assets/hooks/aria-pre-tool-gate.mjs +60 -1
  13. package/dist/assets/hooks/aria-stop-gate.mjs +69 -3
  14. package/dist/assets/hooks/doctrine_trigger_map.json +43 -0
  15. package/dist/assets/hooks/lib/domain-output-quality.mjs +103 -0
  16. package/dist/assets/hooks/lib/skill-autoload-gate.mjs +14 -0
  17. package/dist/assets/opencode-plugins/harness-context/index.js +1 -1
  18. package/dist/assets/opencode-plugins/harness-gate/index.js +114 -10
  19. package/dist/assets/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +14 -0
  20. package/dist/assets/opencode-plugins/harness-outcome/index.js +39 -0
  21. package/dist/assets/opencode-plugins/harness-stop/index.js +234 -139
  22. package/dist/assets/opencode-plugins/harness-stop/lib/domain-output-quality.js +103 -0
  23. package/dist/assets/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +14 -0
  24. package/dist/runtime/codex-bridge.mjs +71 -8
  25. package/dist/runtime/discipline/CLAUDE.md +2 -2
  26. package/dist/runtime/discipline/doctrine_trigger_map.json +43 -0
  27. package/dist/runtime/discipline/skills/aria-harness/aria-harness-onboarding/SKILL.md +3 -3
  28. package/dist/runtime/doctrine_trigger_map.json +43 -0
  29. package/dist/runtime/harness-daemon.mjs +50 -2
  30. package/dist/runtime/hooks/aria-agent-handoff.mjs +247 -0
  31. package/dist/runtime/hooks/aria-agent-ledger-merge.mjs +164 -0
  32. package/dist/runtime/hooks/aria-architect-fallback.mjs +267 -0
  33. package/dist/runtime/hooks/aria-cognition-substrate-binding.mjs +761 -0
  34. package/dist/runtime/hooks/aria-discovery-record.mjs +101 -0
  35. package/dist/runtime/hooks/aria-harness-via-sdk.mjs +544 -0
  36. package/dist/runtime/hooks/aria-import-resolution-gate.mjs +330 -0
  37. package/dist/runtime/hooks/aria-outcome-record.mjs +84 -0
  38. package/dist/runtime/hooks/aria-pre-emit-dryrun.mjs +329 -0
  39. package/dist/runtime/hooks/aria-pre-text-gate.mjs +112 -0
  40. package/dist/runtime/hooks/aria-pre-tool-gate.mjs +2482 -0
  41. package/dist/runtime/hooks/aria-preprompt-consult.mjs +464 -0
  42. package/dist/runtime/hooks/aria-preturn-memory-gate.mjs +647 -0
  43. package/dist/runtime/hooks/aria-repo-doctrine-gate.mjs +429 -0
  44. package/dist/runtime/hooks/aria-stop-gate.mjs +1882 -0
  45. package/dist/runtime/hooks/aria-trigger-autolearn.mjs +229 -0
  46. package/dist/runtime/hooks/aria-userprompt-abandon-detect.mjs +192 -0
  47. package/dist/runtime/hooks/doctrine_trigger_map.json +577 -0
  48. package/dist/runtime/hooks/lib/canonical-lenses.mjs +65 -0
  49. package/dist/runtime/hooks/lib/domain-output-quality.mjs +103 -0
  50. package/dist/runtime/hooks/lib/gate-audit.mjs +43 -0
  51. package/dist/runtime/hooks/lib/gate-loop-state.mjs +50 -0
  52. package/dist/runtime/hooks/lib/hook-message-window.mjs +121 -0
  53. package/dist/runtime/hooks/lib/skill-autoload-gate.mjs +14 -0
  54. package/dist/runtime/hooks/test-aria-preturn-memory-gate.mjs +245 -0
  55. package/dist/runtime/hooks/test-tier-lens-labeling.mjs +367 -0
  56. package/dist/runtime/manifest.json +2 -2
  57. package/dist/runtime/sdk/BUNDLED.json +2 -2
  58. package/dist/runtime/sdk/index.d.ts +48 -0
  59. package/dist/runtime/sdk/index.js +140 -1
  60. package/dist/runtime/sdk/index.js.map +1 -1
  61. package/dist/runtime/sdk/runWithGovernance.d.ts +16 -0
  62. package/dist/runtime/sdk/runWithGovernance.js +54 -0
  63. package/dist/runtime/sdk/runWithGovernance.js.map +1 -0
  64. package/dist/runtime/service.mjs +339 -10
  65. package/dist/sdk/BUNDLED.json +2 -2
  66. package/dist/sdk/index.d.ts +48 -0
  67. package/dist/sdk/index.js +140 -1
  68. package/dist/sdk/index.js.map +1 -1
  69. package/dist/sdk/runWithGovernance.d.ts +16 -0
  70. package/dist/sdk/runWithGovernance.js +54 -0
  71. package/dist/sdk/runWithGovernance.js.map +1 -0
  72. package/hooks/aria-harness-via-sdk.mjs +26 -8
  73. package/hooks/aria-pre-tool-gate.mjs +60 -1
  74. package/hooks/aria-stop-gate.mjs +69 -3
  75. package/hooks/doctrine_trigger_map.json +43 -0
  76. package/hooks/lib/domain-output-quality.mjs +103 -0
  77. package/hooks/lib/skill-autoload-gate.mjs +14 -0
  78. package/opencode-plugins/harness-context/index.js +1 -1
  79. package/opencode-plugins/harness-gate/index.js +114 -10
  80. package/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +14 -0
  81. package/opencode-plugins/harness-outcome/index.js +39 -0
  82. package/opencode-plugins/harness-stop/index.js +234 -139
  83. package/opencode-plugins/harness-stop/lib/domain-output-quality.js +103 -0
  84. package/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +14 -0
  85. package/package.json +12 -5
  86. package/runtime-src/codex-bridge.mjs +71 -8
  87. package/runtime-src/harness-daemon.mjs +50 -2
  88. package/runtime-src/service.mjs +339 -10
  89. package/scripts/bundle-sdk.mjs +2 -0
  90. package/scripts/self-test-harness-gates.mjs +79 -0
  91. package/src/connectors/codebase-awareness.ts +141 -77
  92. package/src/connectors/codex.ts +98 -0
  93. package/src/setup-wizard.ts +105 -25
@@ -0,0 +1,14 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ const RECEIPT_ROOT = process.env.ARIA_SKILL_RECEIPT_DIR || join(tmpdir(), 'aria-skill-receipts');
5
+ const ALIASES = new Map([['deploy', 'aria-harness-deploy'], ['output', 'aria-harness-output-discipline'], ['repo', 'aria-repo-doctrine'], ['forge', 'aria-forge-guardrails']]);
6
+ const RX = { deploy: /deploy-service\.sh|kubectl\s+(?:apply|set|rollout|delete|scale)|helm\s+upgrade|terraform\s+apply|docker\s+push/i, mutationTool: /^(?:edit|write|notebookedit|patch|apply_patch)$/i, mutation: /apply_patch|write file|edit file|modify|delete file|migration|handler|route|runtime|hook|plugin|\btest\b|smoke script/i, strip: /remove|delete|strip|drop|omit|disable|bypass|skip|stub|mock|fake|placeholder|temporary|quick scaffold|band-aid/i, readiness: /production-ready|ready for production|works in general|general readiness|client packages?|npm packages?|SDKs?|runtimes?|harnesses?|release-ready|ship-ready/i, narrow: /single flow|one flow|narrow e2e|covered flow|specific path|widget flow/i, completion: /done|complete|completed|ready|verified|fixed|shipped|implemented|production-ready/i, badProof: /but|except|caveat|remaining|not yet|still|separate|later|blocked|skipped|unresolved|follow-up|failed|failing|error|red|not run|could not verify|untested|no proof|missing proof|without proof/i, advisory: /non-blocking|warning only|warn only|advisory|fall through|falls through|fail open|soft fail|logged and continue|quality gate warning/i, success: /(?:verified|passed|success|successful|green|ok)\s*[:=\-].{0,120}(?:npm|node|playwright|jest|vitest|build|test|lint|typecheck|curl|kubectl|self-test|e2e|probe|smoke)|(?:npm|node|playwright|jest|vitest|build|test|lint|typecheck|curl|kubectl).{0,120}(?:passed|success|successful|green|exit\s*0)/i, resubmit: /re-?submission|resubmit/i, rewrite: /re-?write|rewrite|fix/i, retest: /re-?test|retest|rerun/i, aria: /ARIA console|Aria console|\/chat|aria-pipeline-mcp|aria_chat|escalat(?:e|ion).{0,80}ARIA/i };
7
+ function normalizeSkillName(skill) { return ALIASES.get(String(skill || '').trim()) || String(skill || '').trim(); }
8
+ function sessionDir(sessionId) { return join(RECEIPT_ROOT, encodeURIComponent(String(sessionId || 'unknown'))); }
9
+ function readReceiptSkills(sessionId) { const dir = sessionDir(sessionId); if (!existsSync(dir)) return new Set(); const skills = new Set(); for (const name of readdirSync(dir)) { if (!name.endsWith('.json')) continue; try { const receipt = JSON.parse(readFileSync(join(dir, name), 'utf8')); if (receipt?.skill) skills.add(normalizeSkillName(receipt.skill)); } catch {} } return skills; }
10
+ function readInlineSkills(text) { const skills = new Set(); const value = String(text || ''); for (const match of value.matchAll(/<skill_content\s+name=["']([^"']+)["']/gi)) skills.add(normalizeSkillName(match[1])); return skills; }
11
+ export function recordSkillLoaded({ sessionId, skill, surface = 'unknown', metadata = {} } = {}) { const normalized = normalizeSkillName(skill); if (!normalized) throw new Error('recordSkillLoaded requires a skill name'); const dir = sessionDir(sessionId); mkdirSync(dir, { recursive: true }); const receipt = { skill: normalized, surface, metadata, recordedAt: new Date().toISOString() }; writeFileSync(join(dir, `${encodeURIComponent(normalized)}.json`), `${JSON.stringify(receipt, null, 2)}\n`); return receipt; }
12
+ export function classifyRequiredSkills({ text = '', action = '', toolName = '', filePath = '', isDeploy = false, isMutation = false, isOutputCloseout = false } = {}) { const combined = [text, action, toolName, filePath].filter(Boolean).join('\n'); const required = new Set(); const reasons = []; const recoveryMissing = []; if (isDeploy || RX.deploy.test(combined)) { required.add('aria-harness-deploy'); required.add('aria-forge-guardrails'); reasons.push('deploy/shared-infrastructure action requires fail-closed deploy and forge guardrails'); } if (isMutation || RX.mutationTool.test(toolName)) { required.add('aria-repo-doctrine'); reasons.push('repository/runtime mutation requires repo doctrine'); } if (RX.strip.test(combined)) { required.add('aria-harness-no-stripping'); reasons.push('strip/remove/bypass language requires no-stripping gate'); } if (isOutputCloseout && RX.completion.test(combined)) { required.add('aria-harness-output-discipline'); reasons.push('owner-facing completion/readiness claim requires output discipline'); if (!RX.success.test(combined)) recoveryMissing.push('successful proof from a concrete command/probe'); } if (RX.readiness.test(combined)) { required.add('architecture-decision'); required.add('testing-strategy'); required.add('aria-forge-guardrails'); required.add('aria-harness-output-discipline'); reasons.push('broad production/package/SDK/runtime readiness claim requires architecture, testing, and forge guardrails'); } if (RX.readiness.test(combined) && RX.narrow.test(combined)) { required.add('testing-strategy'); required.add('aria-forge-guardrails'); reasons.push('narrow e2e proof cannot support broad readiness claim without readiness-matrix discipline'); } if (RX.completion.test(combined) && RX.badProof.test(combined)) { required.add('aria-harness-output-discipline'); required.add('aria-forge-guardrails'); reasons.push('completion claim with unresolved or failed proof requires recovery cycle'); if (!RX.resubmit.test(combined)) recoveryMissing.push('re-submission'); if (!RX.rewrite.test(combined)) recoveryMissing.push('re-write'); if (!RX.retest.test(combined)) recoveryMissing.push('re-test'); if (!RX.aria.test(combined)) recoveryMissing.push('ARIA console escalation'); } if (RX.advisory.test(combined)) { required.add('aria-forge-guardrails'); required.add('aria-harness-output-discipline'); reasons.push('advisory/fail-open gate language requires fail-closed hardening discipline'); } return { requiredSkills: [...required].sort(), reasons, recoveryMissing }; }
13
+ export function evaluateSkillGate(options = {}) { const classified = classifyRequiredSkills(options); const text = [options.text, options.action].filter(Boolean).join('\n'); const loaded = new Set([...readReceiptSkills(options.sessionId), ...readInlineSkills(text)]); const missingSkills = classified.requiredSkills.filter((skill) => !loaded.has(skill)); const recoveryMissing = classified.recoveryMissing || []; return { ok: missingSkills.length === 0 && recoveryMissing.length === 0, surface: options.surface || 'unknown', sessionId: options.sessionId || 'unknown', requiredSkills: classified.requiredSkills, loadedSkills: [...loaded].sort(), missingSkills, recoveryMissing, reasons: classified.reasons, autoLoadAvailable: options.autoLoadAvailable === true }; }
14
+ export function formatSkillGateBlock(result = {}) { const missing = Array.isArray(result.missingSkills) ? result.missingSkills : []; const recovery = Array.isArray(result.recoveryMissing) ? result.recoveryMissing : []; const reasons = Array.isArray(result.reasons) ? result.reasons : []; return ['=== ARIA SKILL AUTOLOAD GATE BLOCK ===', `surface: ${result.surface || 'unknown'}`, `missing_skills: ${missing.length ? missing.join(', ') : '(none)'}`, `missing_recovery_cycle: ${recovery.length ? recovery.join(', ') : '(none)'}`, `required_skills: ${(result.requiredSkills || []).join(', ') || '(none)'}`, reasons.length ? `reasons: ${reasons.join(' | ')}` : 'reasons: no classifier reason recorded', 'counter_action: re-submit, re-write, re-test, and escalate through ARIA console until successful proof exists. Do not downgrade this to an advisory warning.'].join('\n'); }
@@ -3,6 +3,7 @@
3
3
  * Routes through the canonical SDK for outcome/garden/ledger writes.
4
4
  */
5
5
  import { existsSync, readFileSync } from 'node:fs';
6
+ import { createHash } from 'node:crypto';
6
7
  import { homedir } from 'node:os';
7
8
  import { join } from 'node:path';
8
9
 
@@ -14,6 +15,7 @@ const SDK_CANDIDATES = [
14
15
  ];
15
16
  const OWNER_TOKEN_PATH = join(HOME, '.aria', 'owner-token');
16
17
  const LICENSE_PATH = join(HOME, '.aria', 'license.json');
18
+ const RUNTIME_URL = (process.env.ARIA_RUNTIME_URL || 'http://127.0.0.1:4319').replace(/\/+$/, '');
17
19
 
18
20
  let _client = null;
19
21
  let _clientError = null;
@@ -46,6 +48,32 @@ function resolveSdkPath() {
46
48
  return SDK_CANDIDATES.find((candidate) => existsSync(candidate)) || '';
47
49
  }
48
50
 
51
+ function makeEvidenceRef(kind, value, metadata = {}) {
52
+ const raw = typeof value === 'string' ? value : JSON.stringify(value ?? null);
53
+ const sha256 = createHash('sha256').update(raw).digest('hex');
54
+ return {
55
+ evidenceId: `ev_${sha256.slice(0, 16)}`,
56
+ kind,
57
+ at: new Date().toISOString(),
58
+ sha256,
59
+ preview: raw.slice(0, 500),
60
+ metadata,
61
+ };
62
+ }
63
+
64
+ async function releaseHiveLease(sessionId, files) {
65
+ const token = resolveToken();
66
+ if (!token || !Array.isArray(files) || files.length === 0) return;
67
+ await fetch(`${RUNTIME_URL}/hive/release`, {
68
+ method: 'POST',
69
+ headers: {
70
+ 'Content-Type': 'application/json',
71
+ Authorization: `Bearer ${token}`,
72
+ },
73
+ body: JSON.stringify({ sessionId, files }),
74
+ }).catch(() => {});
75
+ }
76
+
49
77
  async function getClient() {
50
78
  if (_client) return _client;
51
79
  if (_clientError) return null;
@@ -78,6 +106,15 @@ export default async function HarnessOutcomePlugin(ctx) {
78
106
  : String(input.args?.file_path ?? input.args?.notebook_path ?? '').slice(0, 200) || 'unknown';
79
107
  const success = !output?.error;
80
108
  const sessionId = process.env.ARIA_SESSION_ID || 'opencode';
109
+ const filePath = toolName !== 'Bash' ? String(input.args?.file_path ?? input.args?.notebook_path ?? '') : '';
110
+ await releaseHiveLease(sessionId, filePath ? [filePath] : []);
111
+ const outcomeRef = makeEvidenceRef('opencode_tool_outcome', {
112
+ toolName,
113
+ actionKind,
114
+ actionTarget,
115
+ success,
116
+ output: output ?? null,
117
+ }, { sessionId });
81
118
 
82
119
  // Try SDK first
83
120
  const client = await getClient();
@@ -86,6 +123,7 @@ export default async function HarnessOutcomePlugin(ctx) {
86
123
  await client.recordDiscovery({
87
124
  text: `${actionKind}: ${actionTarget} — ${success ? 'ok' : 'failed'}`,
88
125
  kind: success ? 'observation' : 'defect',
126
+ evidence: JSON.stringify(outcomeRef),
89
127
  source: 'opencode-outcome-plugin',
90
128
  resolution_status: success ? 'resolved' : 'open',
91
129
  sessionId,
@@ -122,6 +160,7 @@ export default async function HarnessOutcomePlugin(ctx) {
122
160
  actionKind,
123
161
  actionTarget,
124
162
  actionShape: { tool: toolName, success },
163
+ evidenceRef: outcomeRef,
125
164
  }),
126
165
  }).catch(() => {});
127
166
  },
@@ -3,8 +3,12 @@
3
3
  * Routes text through Mizan validateOutput() for substrate-backed QC.
4
4
  */
5
5
  import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
6
+ import { createHash } from 'node:crypto';
7
+ import { spawnSync } from 'node:child_process';
6
8
  import { homedir } from 'node:os';
7
9
  import { join } from 'node:path';
10
+ import { analyzeDomainOutputQuality, extractCodeBlocks } from './lib/domain-output-quality.js';
11
+ import { evaluateSkillGate, formatSkillGateBlock } from './lib/skill-autoload-gate.js';
8
12
 
9
13
  const HOME = homedir();
10
14
  const SDK_CANDIDATES = [
@@ -14,6 +18,7 @@ const SDK_CANDIDATES = [
14
18
  ];
15
19
  const OWNER_TOKEN_PATH = join(HOME, '.aria', 'owner-token');
16
20
  const LICENSE_PATH = join(HOME, '.aria', 'license.json');
21
+ const GOVERNANCE_GATE_PATH = join(HOME, '.aria', 'bin', 'aria-governance-gate');
17
22
  const RUNTIME_URL = (process.env.ARIA_RUNTIME_URL || 'http://127.0.0.1:4319').replace(/\/+$/, '');
18
23
  const RECEIPT_DIR = join(HOME, '.opencode', 'aria-mizan-receipts');
19
24
 
@@ -46,6 +51,25 @@ function isGateBlock(error) {
46
51
  return BLOCK_PREFIX_RX.test(String(error?.message || error || ''));
47
52
  }
48
53
 
54
+ function runUniversalGovernanceGate(payload) {
55
+ if (!existsSync(GOVERNANCE_GATE_PATH)) return null;
56
+ const child = spawnSync(GOVERNANCE_GATE_PATH, {
57
+ input: `${JSON.stringify(payload)}\n`,
58
+ encoding: 'utf8',
59
+ maxBuffer: 1024 * 1024,
60
+ });
61
+ const stdout = String(child.stdout || '').trim();
62
+ let result = null;
63
+ try { result = stdout ? JSON.parse(stdout) : null; } catch {}
64
+ if (child.status !== 0 || result?.ok === false || result?.decision === 'block') {
65
+ throw new Error(
66
+ `=== ARIA UNIVERSAL GOVERNANCE GATE BLOCK ===\n\n` +
67
+ `${stdout || child.stderr || 'aria-governance-gate blocked this output.'}`
68
+ );
69
+ }
70
+ return result;
71
+ }
72
+
49
73
  let _client = null;
50
74
  let _clientError = null;
51
75
  let _lastMizanReceipt = null;
@@ -71,6 +95,19 @@ function persistReceiptState(sessionId, payload) {
71
95
  } catch {}
72
96
  }
73
97
 
98
+ function makeEvidenceRef(kind, value, metadata = {}) {
99
+ const raw = typeof value === 'string' ? value : JSON.stringify(value ?? null);
100
+ const sha256 = createHash('sha256').update(raw).digest('hex');
101
+ return {
102
+ evidenceId: `ev_${sha256.slice(0, 16)}`,
103
+ kind,
104
+ at: new Date().toISOString(),
105
+ sha256,
106
+ preview: raw.slice(0, 500),
107
+ metadata,
108
+ };
109
+ }
110
+
74
111
  function resolveToken() {
75
112
  if (process.env.ARIA_HARNESS_TOKEN) return process.env.ARIA_HARNESS_TOKEN;
76
113
  if (process.env.ARIA_API_KEY) return process.env.ARIA_API_KEY;
@@ -137,7 +174,7 @@ async function runtimeValidateOutput(text, sessionId) {
137
174
  return payload.validation || payload.result || payload;
138
175
  }
139
176
 
140
- async function runtimeMizanPost(text, sessionId, context = {}) {
177
+ async function runtimeMizanPost(text, sessionId, context = {}, evidence = {}) {
141
178
  const existing = loadReceiptState(sessionId);
142
179
  const token = resolveToken();
143
180
  if (!token) throw new Error('no token');
@@ -150,10 +187,22 @@ async function runtimeMizanPost(text, sessionId, context = {}) {
150
187
  body: JSON.stringify({
151
188
  sessionId,
152
189
  text,
190
+ evidence,
153
191
  context: {
154
192
  sessionId,
193
+ actor: 'opencode',
194
+ system: 'opencode',
195
+ platform: 'opencode',
196
+ surface: 'opencode-harness-stop',
155
197
  ...context,
156
198
  },
199
+ packetRequest: {
200
+ stage: 'preflight',
201
+ actor: 'opencode',
202
+ system: 'opencode',
203
+ platform: 'opencode',
204
+ message: String(context.message || text || '').slice(0, 1000),
205
+ },
157
206
  parentReceiptId: existing?.receipt?.receiptId || _lastMizanReceipt?.receiptId || null,
158
207
  }),
159
208
  });
@@ -205,160 +254,206 @@ function detectCognitionLenses(text) {
205
254
  export default async function HarnessStopPlugin(ctx) {
206
255
  process.stderr.write('[harness-stop] Active — SDK-backed text-emission gate\n');
207
256
 
208
- return {
209
- 'session.idle': async (event) => {
210
- process.stderr.write(' [strip gate registed — session i\n');
211
- },
212
-
213
- event: async ({ event }) => {
214
- if (event.type !== 'message.updated') return;
215
- const parts = event.properties?.parts || [];
216
- const text = parts
217
- .filter(p => p.type === 'text')
218
- .map(p => p.text || '')
219
- .join('\n');
257
+ async function validateText(text, eventContext = {}) {
258
+ if (!text || text.length < NON_TRIVIAL_MIN_CHARS) return;
259
+ if (TRIVIAL_ACK_RX.test(text.trim())) return;
220
260
 
221
- if (!text || text.length < NON_TRIVIAL_MIN_CHARS) return;
222
- if (TRIVIAL_ACK_RX.test(text.trim())) return;
261
+ const cog = detectCognitionLenses(text);
262
+ const sessionId = eventContext.sessionID || process.env.ARIA_SESSION_ID || process.env.OPENCODE_SESSION_ID || 'opencode';
263
+ runUniversalGovernanceGate({
264
+ sessionId,
265
+ sourceRuntime: 'opencode',
266
+ surface: 'opencode-harness-stop',
267
+ text,
268
+ isOutputCloseout: true,
269
+ evidence: makeEvidenceRef('opencode_assistant_output_candidate', text.slice(0, 8000), { sessionId }),
270
+ });
271
+ const skillGate = evaluateSkillGate({
272
+ sessionId,
273
+ surface: 'opencode-harness-stop',
274
+ text: JSON.stringify(eventContext || {}) + '\n' + text,
275
+ isOutputCloseout: true,
276
+ autoLoadAvailable: false,
277
+ });
278
+ if (!skillGate.ok) {
279
+ throw new Error(
280
+ formatBlockReason(
281
+ '=== ARIA SKILL AUTOLOAD GATE BLOCK ===',
282
+ `${formatSkillGateBlock(skillGate)}\nRequired next step: load ${skillGate.missingSkills.join(', ')} before re-emitting this output.`,
283
+ )
284
+ );
285
+ }
286
+ const outputRef = makeEvidenceRef('opencode_assistant_output', text.slice(0, 8000), { sessionId });
287
+ try {
288
+ const mizan = await runtimeMizanPost(text.slice(0, 8000), sessionId, {
289
+ message: text.slice(0, 1000),
290
+ plannedApproach: 'OpenCode stop gate output review',
291
+ outputRef,
292
+ }, {
293
+ output_ref: outputRef,
294
+ cognition_lens_count: cog.count,
295
+ });
296
+ _lastMizanReceipt = mizan.receipt || _lastMizanReceipt;
297
+ if (_lastMizanReceipt) {
298
+ const existing = loadReceiptState(sessionId) || {};
299
+ persistReceiptState(sessionId, {
300
+ ...existing,
301
+ updatedAt: new Date().toISOString(),
302
+ sessionId,
303
+ postReceipt: _lastMizanReceipt,
304
+ postResult: mizan.result || null,
305
+ postContract: mizan.contract || null,
306
+ postSummary: mizan.summary || null,
307
+ outputRef,
308
+ });
309
+ }
310
+ if (mizan.receipt?.blocked || mizan.result?.fitrahVetoed || mizan.result?.reAuthorSignal) {
311
+ const details = (mizan.result?.notes || ['post-phase blocked']).slice(0, 4).join(' | ');
312
+ throw new Error(
313
+ formatBlockReason('=== ARIA MIZAN POST BLOCK ===', details)
314
+ );
315
+ }
316
+ } catch (e) {
317
+ if (isGateBlock(e)) throw e;
318
+ process.stderr.write(`[harness-stop] mizan/post unavailable: ${e.message}\n`);
319
+ }
223
320
 
224
- const cog = detectCognitionLenses(text);
225
- const sessionId = process.env.ARIA_SESSION_ID || 'opencode';
321
+ const client = await getClient();
322
+ if (client) {
226
323
  try {
227
- const mizan = await runtimeMizanPost(text.slice(0, 8000), sessionId, {
228
- message: text.slice(0, 1000),
229
- plannedApproach: 'OpenCode stop gate output review',
230
- });
231
- _lastMizanReceipt = mizan.receipt || _lastMizanReceipt;
232
- if (_lastMizanReceipt) {
233
- const existing = loadReceiptState(sessionId) || {};
234
- persistReceiptState(sessionId, {
235
- ...existing,
236
- updatedAt: new Date().toISOString(),
237
- sessionId,
238
- postReceipt: _lastMizanReceipt,
239
- postResult: mizan.result || null,
240
- postContract: mizan.contract || null,
241
- postSummary: mizan.summary || null,
242
- });
243
- }
244
- if (mizan.receipt?.blocked || mizan.result?.fitrahVetoed || mizan.result?.reAuthorSignal) {
245
- const details = (mizan.result?.notes || ['post-phase blocked']).slice(0, 4).join(' | ');
324
+ const result = await runtimeValidateOutput(
325
+ text.slice(0, 8000),
326
+ sessionId,
327
+ ).catch(() => client.validateOutput(
328
+ text.slice(0, 8000),
329
+ sessionId,
330
+ ));
331
+ if (result.severity === 'block') {
332
+ throw new Error(
333
+ formatBlockReason(
334
+ '=== ARIA OUTPUT GATE BLOCK ===',
335
+ `${result.violations.length} violations: ${result.violations.join('; ').slice(0, 500)}`,
336
+ )
337
+ );
338
+ } else if (result.severity === 'warn') {
246
339
  throw new Error(
247
- formatBlockReason('=== ARIA MIZAN POST BLOCK ===', details)
340
+ formatBlockReason(
341
+ '=== ARIA OUTPUT GATE BLOCK ===',
342
+ `SDK returned warn for ${result.violations.length} violation(s): ${(result.violations || []).join('; ').slice(0, 500)}. Warnings are fail-closed on owner-facing output.`,
343
+ )
248
344
  );
249
345
  }
346
+ if (result.gateTriggers?.length) {
347
+ process.stderr.write(`[harness-stop] SDK triggers: ${result.gateTriggers.join(', ')}\n`);
348
+ }
349
+ return;
250
350
  } catch (e) {
251
351
  if (isGateBlock(e)) throw e;
252
- process.stderr.write(`[harness-stop] mizan/post unavailable: ${e.message}\n`);
352
+ process.stderr.write(`[harness-stop] SDK validateOutput failed: ${e.message} — falling through to local gate\n`);
253
353
  }
354
+ }
254
355
 
255
- // Try SDK validateOutput() — Mizan classifier-backed validation
256
- const client = await getClient();
257
- if (client) {
258
- try {
259
- const result = await runtimeValidateOutput(
260
- text.slice(0, 8000),
261
- sessionId,
262
- ).catch(() => client.validateOutput(
263
- text.slice(0, 8000),
264
- sessionId,
265
- ));
266
- if (result.severity === 'block') {
267
- throw new Error(
268
- formatBlockReason(
269
- '=== ARIA OUTPUT GATE BLOCK ===',
270
- `${result.violations.length} violations: ${result.violations.join('; ').slice(0, 500)}`,
271
- )
272
- );
273
- } else if (result.severity === 'warn') {
274
- process.stderr.write(
275
- `[harness-stop] SDK WARN — ${result.violations.length} violations (non-blocking)\n`
276
- );
277
- }
278
- // Log gate triggers
279
- if (result.gateTriggers?.length) {
280
- process.stderr.write(`[harness-stop] SDK triggers: ${result.gateTriggers.join(', ')}\n`);
281
- }
282
- return;
283
- } catch (e) {
284
- if (isGateBlock(e)) throw e;
285
- process.stderr.write(`[harness-stop] SDK validateOutput failed: ${e.message} — falling through to local gate\n`);
356
+ const triggerMapPaths = [
357
+ `${HOME}/.aria/runtime/discipline/doctrine_trigger_map.json`,
358
+ `${HOME}/.aria/runtime/doctrine_trigger_map.json`,
359
+ `${HOME}/.opencode/doctrine_trigger_map.json`,
360
+ `${HOME}/.codex/doctrine_trigger_map.json`,
361
+ `${HOME}/.claude/hooks/doctrine_trigger_map.json`,
362
+ `${HOME}/.claude/projects/-home-hamzaibrahim1/memory/doctrine_trigger_map.json`,
363
+ ];
364
+ let driftHits = [];
365
+ try {
366
+ const triggerMapPath = triggerMapPaths.find((candidate) => existsSync(candidate));
367
+ if (triggerMapPath) {
368
+ const triggerMap = JSON.parse(readFileSync(triggerMapPath, 'utf8'));
369
+ const lower = text.toLowerCase();
370
+ for (const t of triggerMap.triggers || []) {
371
+ try {
372
+ const rx = new RegExp(t.trigger, 'i');
373
+ if (rx.test(lower)) {
374
+ const memCited = t.memory && lower.includes(t.memory.replace(/\.md$/, '').toLowerCase());
375
+ if (!memCited) driftHits.push(t.trigger);
376
+ }
377
+ } catch {}
286
378
  }
287
379
  }
380
+ } catch {}
288
381
 
289
- // Local fallback gate
290
- // Scan drift triggers
291
- const triggerMapPaths = [
292
- `${HOME}/.aria/runtime/discipline/doctrine_trigger_map.json`,
293
- `${HOME}/.aria/runtime/doctrine_trigger_map.json`,
294
- `${HOME}/.opencode/doctrine_trigger_map.json`,
295
- `${HOME}/.codex/doctrine_trigger_map.json`,
296
- `${HOME}/.claude/hooks/doctrine_trigger_map.json`,
297
- `${HOME}/.claude/projects/-home-hamzaibrahim1/memory/doctrine_trigger_map.json`,
298
- ];
299
- let driftHits = [];
300
- try {
301
- const triggerMapPath = triggerMapPaths.find((candidate) => existsSync(candidate));
302
- if (triggerMapPath) {
303
- const triggerMap = JSON.parse(readFileSync(triggerMapPath, 'utf8'));
304
- const lower = text.toLowerCase();
305
- for (const t of triggerMap.triggers || []) {
306
- try {
307
- const rx = new RegExp(t.trigger, 'i');
308
- if (rx.test(lower)) {
309
- const memCited = t.memory && lower.includes(t.memory.replace(/\.md$/, '').toLowerCase());
310
- if (!memCited) driftHits.push(t.trigger);
311
- }
312
- } catch {}
313
- }
314
- }
315
- } catch {}
382
+ if (cog.count < REQUIRED_LENSES || driftHits.length >= 2) {
383
+ throw new Error(
384
+ formatBlockReason(
385
+ '=== ARIA LOCAL OUTPUT BLOCK ===',
386
+ `cognition=${cog.count}/${REQUIRED_LENSES}; drift=${driftHits.length}`,
387
+ )
388
+ );
389
+ }
316
390
 
317
- if (cog.count < REQUIRED_LENSES || driftHits.length >= 2) {
318
- throw new Error(
319
- formatBlockReason(
320
- '=== ARIA LOCAL OUTPUT BLOCK ===',
321
- `cognition=${cog.count}/${REQUIRED_LENSES}; drift=${driftHits.length}`,
322
- )
323
- );
324
- }
391
+ if (DECISION_SIGNAL_RX.test(text) && !APPLIED_COGNITION_RX.test(text)) {
392
+ throw new Error(
393
+ formatBlockReason(
394
+ '=== ARIA LOCAL OUTPUT BLOCK ===',
395
+ 'decision-bearing output lacks required applied_cognition binding fields',
396
+ )
397
+ );
398
+ }
325
399
 
326
- if (DECISION_SIGNAL_RX.test(text) && !APPLIED_COGNITION_RX.test(text)) {
327
- throw new Error(
328
- formatBlockReason(
329
- '=== ARIA LOCAL OUTPUT BLOCK ===',
330
- 'decision-bearing output lacks required applied_cognition binding fields',
331
- )
332
- );
333
- }
400
+ const domainQuality = analyzeDomainOutputQuality(text, { codeBlocks: extractCodeBlocks(text) });
401
+ if (domainQuality.blockers.length > 0) {
402
+ throw new Error(
403
+ formatBlockReason(
404
+ '=== ARIA LOCAL OUTPUT BLOCK ===',
405
+ `domain output QA (${domainQuality.domains.join(', ') || 'general'}): ${domainQuality.blockers.join('; ')}`,
406
+ )
407
+ );
408
+ }
334
409
 
335
- try {
336
- const existing = loadReceiptState(sessionId);
337
- await runtimeDecisionLog({
338
- decision_type: 'turn_action',
339
- category: 'agentic_execution',
340
- context: `opencode stop-gate turn (chars=${text.length})`,
341
- decision: 'turn completed',
342
- reasoning: cog.count > 0
343
- ? `Cognition lenses applied: ${cog.names.join(', ')}.`
344
- : 'No explicit cognition block in turn.',
345
- outcome: 'pending',
346
- outcome_details: {
347
- expected: null,
348
- immediate_actual: null,
349
- anchors: [],
350
- },
351
- expected_outcome: null,
352
- metadata: {
353
- pre_receipt_id: existing?.receipt?.receiptId || null,
354
- post_receipt_id: _lastMizanReceipt?.receiptId || null,
355
- },
356
- source: 'opencode-stop-gate-runtime',
357
- model_used: process.env.OPENCODE_MODEL || 'opencode',
358
- });
359
- } catch (e) {
360
- process.stderr.write(`[harness-stop] decision/log unavailable: ${e.message}\n`);
361
- }
410
+ try {
411
+ const existing = loadReceiptState(sessionId);
412
+ await runtimeDecisionLog({
413
+ decision_type: 'turn_action',
414
+ category: 'agentic_execution',
415
+ context: `opencode stop-gate turn (chars=${text.length})`,
416
+ decision: 'turn completed',
417
+ reasoning: cog.count > 0
418
+ ? `Cognition lenses applied: ${cog.names.join(', ')}.`
419
+ : 'No explicit cognition block in turn.',
420
+ outcome: 'pending',
421
+ outcome_details: {
422
+ expected: null,
423
+ immediate_actual: null,
424
+ anchors: [],
425
+ },
426
+ expected_outcome: null,
427
+ metadata: {
428
+ pre_receipt_id: existing?.receipt?.receiptId || null,
429
+ post_receipt_id: _lastMizanReceipt?.receiptId || null,
430
+ output_ref: outputRef,
431
+ },
432
+ source: 'opencode-stop-gate-runtime',
433
+ model_used: process.env.OPENCODE_MODEL || 'opencode',
434
+ });
435
+ } catch (e) {
436
+ process.stderr.write(`[harness-stop] decision/log unavailable: ${e.message}\n`);
437
+ }
438
+ }
439
+
440
+ return {
441
+ 'session.idle': async (event) => {
442
+ process.stderr.write('[harness-stop] session idle heartbeat — text-emission gate registered\n');
443
+ },
444
+
445
+ event: async ({ event }) => {
446
+ if (event.type !== 'message.updated') return;
447
+ const parts = event.properties?.parts || [];
448
+ const text = parts
449
+ .filter(p => p.type === 'text')
450
+ .map(p => p.text || '')
451
+ .join('\n');
452
+ await validateText(text, { event });
453
+ },
454
+
455
+ 'experimental.text.complete': async (input, output) => {
456
+ await validateText(String(output?.text || ''), input || {});
362
457
  },
363
458
  };
364
459
  }
@@ -0,0 +1,103 @@
1
+ // Domain-aware output QA for Aria stop gates.
2
+ // Deterministic local checks complement remote Mizan; they do not replace it.
3
+
4
+ const DOMAIN_RULES = [
5
+ {
6
+ domain: 'code',
7
+ signal: /```|\b(?:function|class|interface|type|import|export|npm test|typecheck|eslint|jest|vitest|tsx?|jsx?|\.ts|\.tsx|\.js|\.py)\b/i,
8
+ },
9
+ {
10
+ domain: 'ui',
11
+ signal: /\b(?:ui|ux|frontend|react|component|page|screen|modal|form|button|layout|tailwind|css|html|mobile|responsive|accessib|aria-label|keyboard|focus state|dark mode)\b/i,
12
+ },
13
+ {
14
+ domain: 'beauty',
15
+ signal: /\b(?:beauty|beautiful|polish|visual|aesthetic|elegant|layout|typography|spacing|hierarchy|composition|brand|design language|modern|clean)\b/i,
16
+ },
17
+ {
18
+ domain: 'security',
19
+ signal: /\b(?:security|auth|token|secret|credential|password|jwt|oauth|csrf|xss|sql injection|permission|role|cors|sanitize|encrypt|webhook signature)\b/i,
20
+ },
21
+ {
22
+ domain: 'ops',
23
+ signal: /\b(?:deploy|rollout|rollback|kubernetes|kubectl|docker|image|pod|health check|slo|alert|log|metric|trace|env var|migration|release)\b/i,
24
+ },
25
+ {
26
+ domain: 'product',
27
+ signal: /\b(?:user flow|customer|workflow|conversion|pricing|onboarding|checkout|activation|retention|business|persona|job-to-be-done|acceptance criteria)\b/i,
28
+ },
29
+ {
30
+ domain: 'writing',
31
+ signal: /\b(?:summary|explain|docs|readme|copy|email|post|article|message|final answer|status report|release notes)\b/i,
32
+ },
33
+ ];
34
+
35
+ function hasAny(text, patterns) {
36
+ return patterns.some((pattern) => pattern.test(text));
37
+ }
38
+
39
+ function lineHits(text, rx, label) {
40
+ const hits = [];
41
+ const lines = String(text || '').split('\n');
42
+ for (let i = 0; i < lines.length; i++) {
43
+ if (rx.test(lines[i])) hits.push(`${label} at line ${i + 1}`);
44
+ }
45
+ return hits;
46
+ }
47
+
48
+ export function extractCodeBlocks(text) {
49
+ return [...String(text || '').matchAll(/```[a-z0-9_-]*\n([\s\S]*?)```/gi)].map((m) => m[1] || '');
50
+ }
51
+
52
+ export function analyzeDomainOutputQuality(text, options = {}) {
53
+ const source = String(text || '');
54
+ const lower = source.toLowerCase();
55
+ const codeBlocks = options.codeBlocks || extractCodeBlocks(source);
56
+ const domains = DOMAIN_RULES.filter((rule) => rule.signal.test(source)).map((rule) => rule.domain);
57
+ const blockers = [];
58
+ const warnings = [];
59
+
60
+ if (domains.includes('code')) {
61
+ for (const hit of lineHits(source, /\b(?:TODO|FIXME|XXX|implementation pending|not implemented|coming soon|placeholder)\b/i, 'code placeholder semantics')) blockers.push(hit);
62
+ for (const block of codeBlocks) {
63
+ if (/@ts-expect-error|@ts-ignore/.test(block)) blockers.push('code: type suppression instead of fixing the type contract');
64
+ if (/catch\s*\([^)]*\)\s*\{\s*(?:return\s+(?:''|""|null|undefined|\[\]|\{\})|\}\s*$|\/\/[^\n]*$)/m.test(block)) blockers.push('code: silent or empty catch block hides runtime failure');
65
+ if (/console\.log\(/.test(block) && !/\/\/\s*(?:debug|log)/i.test(block)) warnings.push('code: console.log appears in shipped code without debug intent');
66
+ }
67
+ }
68
+
69
+ if (domains.includes('ui')) {
70
+ if (!hasAny(source, [/\b(?:mobile|responsive|breakpoint|small screen|desktop)\b/i])) blockers.push('ui: UI/design output must address desktop and mobile responsiveness');
71
+ if (!hasAny(source, [/\b(?:accessib|aria-|keyboard|focus|screen reader|semantic html|label)\b/i])) blockers.push('ui: UI/design output must address accessibility, focus, labels, or semantic structure');
72
+ if (/\b(?:button|input|form|modal|menu|dialog)\b/i.test(source) && !/\b(?:focus|keyboard|aria-|label|escape|tab order)\b/i.test(source)) blockers.push('ui: interactive elements need keyboard/focus/accessibility behavior');
73
+ }
74
+
75
+ if (domains.includes('beauty')) {
76
+ if (/\b(?:clean|modern|beautiful|polished|nice|sleek)\b/i.test(source) && !hasAny(source, [/\b(?:spacing|typography|contrast|hierarchy|rhythm|composition|palette|motion|density|visual language)\b/i])) blockers.push('beauty: aesthetic claim lacks concrete visual language such as spacing, typography, contrast, hierarchy, palette, or composition');
77
+ }
78
+
79
+ if (domains.includes('security')) {
80
+ if (/\b(?:token|secret|credential|password|api key|jwt)\b/i.test(source) && !/\b(?:redact|mask|env|secret store|do not log|rotate|least privilege)\b/i.test(source)) blockers.push('security: secrets/tokens require redaction, env/secret-store handling, no logging, or rotation guidance');
81
+ if (/\b(?:auth|permission|role|admin|tenant)\b/i.test(source) && !/\b(?:authorize|authorization|least privilege|tenant isolation|deny by default|role check)\b/i.test(source)) warnings.push('security: auth/permission output should state authorization and isolation checks');
82
+ }
83
+
84
+ if (domains.includes('ops')) {
85
+ if (/\b(?:deploy|rollout|release|migration|kubectl|docker push)\b/i.test(source) && !/\b(?:rollback|health|smoke|verify|readiness|observability|monitor)\b/i.test(source)) blockers.push('ops: deploy/release output must include rollback plus health/smoke/readiness verification');
86
+ if (/\b(?:env var|config|secret)\b/i.test(source) && !/\b(?:scope|owner|runtime|restart|reload|secret)\b/i.test(source)) warnings.push('ops: runtime config changes should name scope and reload/restart expectations');
87
+ }
88
+
89
+ if (domains.includes('product')) {
90
+ if (!/\b(?:user|customer|operator|admin|persona|workflow|acceptance criteria|success metric|job-to-be-done)\b/i.test(source)) warnings.push('product: product output should bind to a user/workflow and success criterion');
91
+ }
92
+
93
+ if (domains.includes('writing')) {
94
+ if (/\b(?:done|fixed|verified|published|deployed|passed|complete)\b/i.test(lower) && !/\b(?:verified|observed|passed|evidence|output|registry|status|unverified|not verified)\b/i.test(lower)) blockers.push('writing: completion claim needs observed evidence or explicit unverified boundary');
95
+ if (/\b(?:should work|probably|presumably|i assume)\b/i.test(source)) blockers.push('writing: uncertainty must be stated as an evidence boundary, not an assumption');
96
+ }
97
+
98
+ return {
99
+ domains: [...new Set(domains)],
100
+ blockers: [...new Set(blockers)],
101
+ warnings: [...new Set(warnings)],
102
+ };
103
+ }