@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,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
+ }
@@ -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'); }
@@ -2,7 +2,7 @@
2
2
  * Aria Harness Context Plugin for OpenCode.
3
3
  *
4
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
5
+ * session start. Routes through the canonical @aria_asi/harness-http-client SDK
6
6
  * via the inject-context.mjs script that ships alongside this plugin.
7
7
  *
8
8
  * Distribution: this dir is installed by `aria connect` (via connectors/
@@ -3,8 +3,11 @@
3
3
  * Routes through HTTPHarnessClient SDK for substrate-backed validation.
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 { evaluateSkillGate, formatSkillGateBlock } from './lib/skill-autoload-gate.js';
8
11
 
9
12
  const HOME = homedir();
10
13
  const SDK_CANDIDATES = [
@@ -14,6 +17,7 @@ const SDK_CANDIDATES = [
14
17
  ];
15
18
  const OWNER_TOKEN_PATH = join(HOME, '.aria', 'owner-token');
16
19
  const LICENSE_PATH = join(HOME, '.aria', 'license.json');
20
+ const GOVERNANCE_GATE_PATH = join(HOME, '.aria', 'bin', 'aria-governance-gate');
17
21
  const RUNTIME_URL = (process.env.ARIA_RUNTIME_URL || 'http://127.0.0.1:4319').replace(/\/+$/, '');
18
22
  const RECEIPT_DIR = join(HOME, '.opencode', 'aria-mizan-receipts');
19
23
 
@@ -70,6 +74,19 @@ function persistReceipt(sessionId, payload) {
70
74
  } catch {}
71
75
  }
72
76
 
77
+ function makeEvidenceRef(kind, value, metadata = {}) {
78
+ const raw = typeof value === 'string' ? value : JSON.stringify(value ?? null);
79
+ const sha256 = createHash('sha256').update(raw).digest('hex');
80
+ return {
81
+ evidenceId: `ev_${sha256.slice(0, 16)}`,
82
+ kind,
83
+ at: new Date().toISOString(),
84
+ sha256,
85
+ preview: raw.slice(0, 500),
86
+ metadata,
87
+ };
88
+ }
89
+
73
90
  function countInlineCognitionLenses(text) {
74
91
  const seen = new Set();
75
92
  for (const match of String(text || '').matchAll(INLINE_LENS_RX)) {
@@ -79,6 +96,25 @@ function countInlineCognitionLenses(text) {
79
96
  return seen.size;
80
97
  }
81
98
 
99
+ function runUniversalGovernanceGate(payload) {
100
+ if (!existsSync(GOVERNANCE_GATE_PATH)) return null;
101
+ const child = spawnSync(GOVERNANCE_GATE_PATH, {
102
+ input: `${JSON.stringify(payload)}\n`,
103
+ encoding: 'utf8',
104
+ maxBuffer: 1024 * 1024,
105
+ });
106
+ const stdout = String(child.stdout || '').trim();
107
+ let result = null;
108
+ try { result = stdout ? JSON.parse(stdout) : null; } catch {}
109
+ if (child.status !== 0 || result?.ok === false || result?.decision === 'block') {
110
+ throw new Error(
111
+ `=== ARIA UNIVERSAL GOVERNANCE GATE BLOCK ===\n\n` +
112
+ `${stdout || child.stderr || 'aria-governance-gate blocked this action.'}`
113
+ );
114
+ }
115
+ return result;
116
+ }
117
+
82
118
  function resolveToken() {
83
119
  if (process.env.ARIA_HARNESS_TOKEN) return process.env.ARIA_HARNESS_TOKEN;
84
120
  if (process.env.ARIA_API_KEY) return process.env.ARIA_API_KEY;
@@ -127,7 +163,7 @@ async function getClient() {
127
163
  return _client;
128
164
  }
129
165
 
130
- async function runtimeCheckAction(action, target) {
166
+ async function runtimeCheckAction(action, target, metadata = {}) {
131
167
  const token = resolveToken();
132
168
  if (!token) throw new Error('no token');
133
169
  const response = await fetch(`${RUNTIME_URL}/check-action`, {
@@ -136,7 +172,7 @@ async function runtimeCheckAction(action, target) {
136
172
  'Content-Type': 'application/json',
137
173
  Authorization: `Bearer ${token}`,
138
174
  },
139
- body: JSON.stringify({ action, target }),
175
+ body: JSON.stringify({ action, target, ...metadata }),
140
176
  });
141
177
  if (!response.ok) {
142
178
  const body = await response.text().catch(() => response.statusText);
@@ -169,28 +205,66 @@ export default async function HarnessGatePlugin(ctx) {
169
205
 
170
206
  return {
171
207
  'tool.execute.before': async (input, output) => {
172
- const toolName = input.tool;
208
+ const args = output?.args ?? input.args ?? {};
209
+ const rawToolName = String(input.tool || input.name || input.type || '');
210
+ const normalizedToolName = rawToolName.toLowerCase();
211
+ const toolName = normalizedToolName.includes('bash') ? 'Bash'
212
+ : normalizedToolName.includes('edit') ? 'Edit'
213
+ : normalizedToolName.includes('write') ? 'Write'
214
+ : rawToolName;
173
215
  if (!['Bash', 'Edit', 'Write', 'NotebookEdit'].includes(toolName)) return;
174
216
 
175
- const cmd = toolName === 'Bash' ? String(input.args?.command ?? '') : '';
176
- const filePath = toolName !== 'Bash' ? String(input.args?.file_path ?? input.args?.notebook_path ?? '') : '';
217
+ const cmd = toolName === 'Bash' ? String(args?.command ?? '') : '';
218
+ const filePath = toolName !== 'Bash' ? String(args?.file_path ?? args?.filePath ?? args?.notebook_path ?? args?.notebookPath ?? '') : '';
177
219
  const cmdPreview = toolName === 'Bash' ? cmd.slice(0, 80) : `${toolName} ${filePath || '(no path)'}`.slice(0, 80);
178
220
 
179
- // Trivial reads pass
180
- if (toolName === 'Bash' && TRIVIAL_BASH_RX.test(cmd) && cmd.length < 200) return;
181
- if (toolName === 'Bash' && cmd.length < SHORT_BASH_LIMIT) return;
182
221
  const destructive = DESTRUCTIVE_PATTERNS.find(({ rx }) => rx.test(cmd));
183
222
  const deploy = DEPLOY_PATTERNS.find(({ rx }) => rx.test(cmd));
184
223
  const isFileMutation = ['Edit', 'Write', 'NotebookEdit'].includes(toolName) && filePath;
185
224
 
225
+ // Trivial reads pass only after high-risk patterns are classified.
226
+ if (!destructive && !deploy && toolName === 'Bash' && TRIVIAL_BASH_RX.test(cmd) && cmd.length < 200) return;
227
+ if (!destructive && !deploy && toolName === 'Bash' && cmd.length < SHORT_BASH_LIMIT) return;
228
+
186
229
  if (!destructive && !deploy && !isFileMutation) return;
187
230
 
188
231
  // Try SDK checkAction() first — substrate-backed validation
189
232
  const client = await getClient();
190
- const sessionId = process.env.ARIA_SESSION_ID || 'opencode';
233
+ const sessionId = input.sessionID || process.env.ARIA_SESSION_ID || process.env.OPENCODE_SESSION_ID || 'opencode';
191
234
  const label = destructive?.name || deploy?.name || `${toolName}:${filePath?.split('/').pop() || ''}`;
192
235
  const action = toolName === 'Bash' ? 'bash' : 'edit';
193
236
  const target = toolName === 'Bash' ? cmd.slice(0, 200) : filePath.slice(0, 200);
237
+ const actionRef = makeEvidenceRef('opencode_tool_request', { toolName, action, target }, { sessionId, label });
238
+ runUniversalGovernanceGate({
239
+ sessionId,
240
+ sourceRuntime: 'opencode',
241
+ surface: 'opencode-harness-gate',
242
+ text: JSON.stringify({ toolName, action, target, args }).slice(0, 8000),
243
+ action: cmd,
244
+ toolName,
245
+ filePath,
246
+ isDeploy: Boolean(deploy),
247
+ isMutation: Boolean(isFileMutation),
248
+ evidence: actionRef,
249
+ });
250
+ const skillGate = evaluateSkillGate({
251
+ sessionId,
252
+ surface: 'opencode-harness-gate',
253
+ text: JSON.stringify(input || {}),
254
+ action: cmd,
255
+ toolName,
256
+ filePath,
257
+ isDeploy: Boolean(deploy),
258
+ isMutation: Boolean(isFileMutation),
259
+ autoLoadAvailable: false,
260
+ });
261
+ if (!skillGate.ok) {
262
+ throw new Error(
263
+ `=== ARIA SKILL AUTOLOAD GATE: ${label} ===\n\n` +
264
+ `${formatSkillGateBlock(skillGate)}\n\n` +
265
+ `Required next step: call the skill loader for ${skillGate.missingSkills.join(', ')} before retrying this tool.`
266
+ );
267
+ }
194
268
  const rationale =
195
269
  destructive ? `High-risk action ${label} requested in OpenCode and must satisfy Mizan truth, protection, and quality before execution.`
196
270
  : deploy ? `Deployment action ${label} requested in OpenCode and must satisfy Mizan survivability before execution.`
@@ -201,10 +275,21 @@ export default async function HarnessGatePlugin(ctx) {
201
275
  sessionId,
202
276
  context: {
203
277
  sessionId,
278
+ actor: 'opencode',
279
+ system: 'opencode',
280
+ platform: 'opencode',
281
+ surface: 'opencode-harness-gate',
204
282
  message: cmdPreview,
205
283
  intendedAction: target || cmdPreview,
206
284
  rationale,
207
285
  },
286
+ packetRequest: {
287
+ stage: 'preflight',
288
+ actor: 'opencode',
289
+ system: 'opencode',
290
+ platform: 'opencode',
291
+ message: cmdPreview,
292
+ },
208
293
  });
209
294
  _lastMizanReceipt = pre.receipt || null;
210
295
  if (_lastMizanReceipt) {
@@ -215,6 +300,7 @@ export default async function HarnessGatePlugin(ctx) {
215
300
  preResult: pre.result || null,
216
301
  preContract: pre.contract || null,
217
302
  preSummary: pre.summary || null,
303
+ actionRef,
218
304
  });
219
305
  }
220
306
  _lastActionSummary = cmdPreview;
@@ -247,8 +333,25 @@ export default async function HarnessGatePlugin(ctx) {
247
333
 
248
334
  if (client) {
249
335
  try {
250
- const check = await runtimeCheckAction(action, target).catch(() => client.checkAction(action, target));
336
+ const check = await runtimeCheckAction(action, target, {
337
+ sessionId,
338
+ actor: 'opencode',
339
+ roleProfile: process.env.OPENCODE_ARIA_ROLE_PROFILE || process.env.ARIA_ROLE_PROFILE || null,
340
+ files: filePath ? [filePath] : [],
341
+ requireVerify: Boolean(destructive || deploy),
342
+ verifyText: String(args?.verifyText || args?.verifyBlock || args?.verify || ''),
343
+ }).catch(() => client.checkAction(action, target));
251
344
  if (!check.allowed) {
345
+ if (check.queued) {
346
+ throw new Error(
347
+ `=== ARIA TOOL LANE QUEUED: ${label} ===\n\n` +
348
+ `Immediate execution paused; action was queued instead of failed.\n` +
349
+ `Reason: ${check.reason || 'tool lane contention'}\n` +
350
+ `Job: ${check.queue?.jobId || 'unknown'} (${check.queue?.status || 'queued'})\n` +
351
+ `Queue depth: ${check.queue?.queueDepth ?? 'unknown'}\n\n` +
352
+ `Recovery: retry after overlapping Hive lease expires, or let the tool-lane worker claim the queued job.`
353
+ );
354
+ }
252
355
  throw new Error(
253
356
  `=== ARIA SD GATE: ${label} ===\n\n` +
254
357
  `Blocked by substrate gate: ${check.reason || 'no reason provided'}\n` +
@@ -258,6 +361,7 @@ export default async function HarnessGatePlugin(ctx) {
258
361
  }
259
362
  return;
260
363
  } catch (e) {
364
+ if (e.message.startsWith('=== ARIA TOOL LANE QUEUED')) throw e;
261
365
  if (e.message.startsWith('=== ARIA SD GATE')) throw e;
262
366
  // SDK unreachable — fall through to local gate below
263
367
  process.stderr.write(`[harness-gate] SDK checkAction failed: ${e.message} — falling through to local gate\n`);
@@ -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
  },