@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.
- package/dist/aria-connector/src/connectors/codebase-awareness.d.ts +8 -1
- package/dist/aria-connector/src/connectors/codebase-awareness.d.ts.map +1 -1
- package/dist/aria-connector/src/connectors/codebase-awareness.js +126 -71
- package/dist/aria-connector/src/connectors/codebase-awareness.js.map +1 -1
- package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -1
- package/dist/aria-connector/src/connectors/codex.js +98 -0
- package/dist/aria-connector/src/connectors/codex.js.map +1 -1
- package/dist/aria-connector/src/setup-wizard.d.ts.map +1 -1
- package/dist/aria-connector/src/setup-wizard.js +91 -24
- package/dist/aria-connector/src/setup-wizard.js.map +1 -1
- package/dist/assets/hooks/aria-harness-via-sdk.mjs +26 -8
- package/dist/assets/hooks/aria-pre-tool-gate.mjs +60 -1
- package/dist/assets/hooks/aria-stop-gate.mjs +69 -3
- package/dist/assets/hooks/doctrine_trigger_map.json +43 -0
- package/dist/assets/hooks/lib/domain-output-quality.mjs +103 -0
- package/dist/assets/hooks/lib/skill-autoload-gate.mjs +14 -0
- package/dist/assets/opencode-plugins/harness-context/index.js +1 -1
- package/dist/assets/opencode-plugins/harness-gate/index.js +114 -10
- package/dist/assets/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +14 -0
- package/dist/assets/opencode-plugins/harness-outcome/index.js +39 -0
- package/dist/assets/opencode-plugins/harness-stop/index.js +234 -139
- package/dist/assets/opencode-plugins/harness-stop/lib/domain-output-quality.js +103 -0
- package/dist/assets/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +14 -0
- package/dist/runtime/codex-bridge.mjs +71 -8
- package/dist/runtime/discipline/CLAUDE.md +2 -2
- package/dist/runtime/discipline/doctrine_trigger_map.json +43 -0
- package/dist/runtime/discipline/skills/aria-harness/aria-harness-onboarding/SKILL.md +3 -3
- package/dist/runtime/doctrine_trigger_map.json +43 -0
- package/dist/runtime/harness-daemon.mjs +50 -2
- package/dist/runtime/hooks/aria-agent-handoff.mjs +247 -0
- package/dist/runtime/hooks/aria-agent-ledger-merge.mjs +164 -0
- package/dist/runtime/hooks/aria-architect-fallback.mjs +267 -0
- package/dist/runtime/hooks/aria-cognition-substrate-binding.mjs +761 -0
- package/dist/runtime/hooks/aria-discovery-record.mjs +101 -0
- package/dist/runtime/hooks/aria-harness-via-sdk.mjs +544 -0
- package/dist/runtime/hooks/aria-import-resolution-gate.mjs +330 -0
- package/dist/runtime/hooks/aria-outcome-record.mjs +84 -0
- package/dist/runtime/hooks/aria-pre-emit-dryrun.mjs +329 -0
- package/dist/runtime/hooks/aria-pre-text-gate.mjs +112 -0
- package/dist/runtime/hooks/aria-pre-tool-gate.mjs +2482 -0
- package/dist/runtime/hooks/aria-preprompt-consult.mjs +464 -0
- package/dist/runtime/hooks/aria-preturn-memory-gate.mjs +647 -0
- package/dist/runtime/hooks/aria-repo-doctrine-gate.mjs +429 -0
- package/dist/runtime/hooks/aria-stop-gate.mjs +1882 -0
- package/dist/runtime/hooks/aria-trigger-autolearn.mjs +229 -0
- package/dist/runtime/hooks/aria-userprompt-abandon-detect.mjs +192 -0
- package/dist/runtime/hooks/doctrine_trigger_map.json +577 -0
- package/dist/runtime/hooks/lib/canonical-lenses.mjs +65 -0
- package/dist/runtime/hooks/lib/domain-output-quality.mjs +103 -0
- package/dist/runtime/hooks/lib/gate-audit.mjs +43 -0
- package/dist/runtime/hooks/lib/gate-loop-state.mjs +50 -0
- package/dist/runtime/hooks/lib/hook-message-window.mjs +121 -0
- package/dist/runtime/hooks/lib/skill-autoload-gate.mjs +14 -0
- package/dist/runtime/hooks/test-aria-preturn-memory-gate.mjs +245 -0
- package/dist/runtime/hooks/test-tier-lens-labeling.mjs +367 -0
- package/dist/runtime/manifest.json +2 -2
- package/dist/runtime/sdk/BUNDLED.json +2 -2
- package/dist/runtime/sdk/index.d.ts +48 -0
- package/dist/runtime/sdk/index.js +140 -1
- package/dist/runtime/sdk/index.js.map +1 -1
- package/dist/runtime/sdk/runWithGovernance.d.ts +16 -0
- package/dist/runtime/sdk/runWithGovernance.js +54 -0
- package/dist/runtime/sdk/runWithGovernance.js.map +1 -0
- package/dist/runtime/service.mjs +339 -10
- package/dist/sdk/BUNDLED.json +2 -2
- package/dist/sdk/index.d.ts +48 -0
- package/dist/sdk/index.js +140 -1
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/runWithGovernance.d.ts +16 -0
- package/dist/sdk/runWithGovernance.js +54 -0
- package/dist/sdk/runWithGovernance.js.map +1 -0
- package/hooks/aria-harness-via-sdk.mjs +26 -8
- package/hooks/aria-pre-tool-gate.mjs +60 -1
- package/hooks/aria-stop-gate.mjs +69 -3
- package/hooks/doctrine_trigger_map.json +43 -0
- package/hooks/lib/domain-output-quality.mjs +103 -0
- package/hooks/lib/skill-autoload-gate.mjs +14 -0
- package/opencode-plugins/harness-context/index.js +1 -1
- package/opencode-plugins/harness-gate/index.js +114 -10
- package/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +14 -0
- package/opencode-plugins/harness-outcome/index.js +39 -0
- package/opencode-plugins/harness-stop/index.js +234 -139
- package/opencode-plugins/harness-stop/lib/domain-output-quality.js +103 -0
- package/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +14 -0
- package/package.json +12 -5
- package/runtime-src/codex-bridge.mjs +71 -8
- package/runtime-src/harness-daemon.mjs +50 -2
- package/runtime-src/service.mjs +339 -10
- package/scripts/bundle-sdk.mjs +2 -0
- package/scripts/self-test-harness-gates.mjs +79 -0
- package/src/connectors/codebase-awareness.ts +141 -77
- package/src/connectors/codex.ts +98 -0
- package/src/setup-wizard.ts +105 -25
|
@@ -52,11 +52,30 @@ import {
|
|
|
52
52
|
normalizeContent,
|
|
53
53
|
normalizeRole,
|
|
54
54
|
} from './lib/hook-message-window.mjs';
|
|
55
|
+
import { evaluateSkillGate, formatSkillGateBlock } from './lib/skill-autoload-gate.mjs';
|
|
55
56
|
|
|
56
57
|
const HOME = process.env.HOME || '/tmp';
|
|
57
58
|
const LOG = `${HOME}/.claude/aria-pre-tool-gate.log`;
|
|
58
59
|
const HEARTBEAT = `${HOME}/.claude/aria-pre-tool-gate-heartbeat.jsonl`;
|
|
59
60
|
const GATE_LOOP_STATE_PATH = `${HOME}/.claude/.aria-gate-loop-state.json`;
|
|
61
|
+
const GOVERNANCE_GATE_PATH = `${HOME}/.aria/bin/aria-governance-gate`;
|
|
62
|
+
|
|
63
|
+
function runUniversalGovernanceGate(payload) {
|
|
64
|
+
if (!existsSync(GOVERNANCE_GATE_PATH)) return null;
|
|
65
|
+
const child = spawnSync(GOVERNANCE_GATE_PATH, {
|
|
66
|
+
input: `${JSON.stringify(payload)}\n`,
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
maxBuffer: 1024 * 1024,
|
|
69
|
+
});
|
|
70
|
+
const stdout = String(child.stdout || '').trim();
|
|
71
|
+
let result = null;
|
|
72
|
+
try { result = stdout ? JSON.parse(stdout) : null; } catch {}
|
|
73
|
+
if (child.status !== 0 || result?.ok === false || result?.decision === 'block') {
|
|
74
|
+
const reason = stdout || child.stderr || 'aria-governance-gate blocked this action.';
|
|
75
|
+
throw new Error(`=== ARIA UNIVERSAL GOVERNANCE GATE BLOCK ===\n\n${reason}`);
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
60
79
|
|
|
61
80
|
// ── Heartbeat OUTSIDE the crash boundary (doctrine #123) ───────────────
|
|
62
81
|
// Per feedback_ledger_writes_outside_crash_boundary.md + doctrine #124
|
|
@@ -1287,6 +1306,43 @@ const sessionId =
|
|
|
1287
1306
|
(transcriptPath ? transcriptPath.split('/').pop()?.replace(/\.[^.]+$/, '') : null) ??
|
|
1288
1307
|
'claude-code-unknown';
|
|
1289
1308
|
|
|
1309
|
+
const skillGate = evaluateSkillGate({
|
|
1310
|
+
sessionId,
|
|
1311
|
+
surface: 'claude-pre-tool-gate',
|
|
1312
|
+
text: [JSON.stringify(event.messages || []), unionText, JSON.stringify(toolInput || {})].join('\n'),
|
|
1313
|
+
action: cmd,
|
|
1314
|
+
toolName,
|
|
1315
|
+
filePath,
|
|
1316
|
+
isDeploy: Boolean(deployMatched),
|
|
1317
|
+
isMutation: toolName !== 'Bash',
|
|
1318
|
+
autoLoadAvailable: false,
|
|
1319
|
+
});
|
|
1320
|
+
if (!skillGate.ok) {
|
|
1321
|
+
const reason = formatSkillGateBlock(skillGate);
|
|
1322
|
+
audit('block-missing-skill-receipt', `${skillGate.missingSkills.join(',')} ${cmdPreview}`);
|
|
1323
|
+
emitBlock(reason, { source: 'pre-tool/skill-autoload', tool: toolName, lensCount, requiredLenses: REQUIRED_LENSES });
|
|
1324
|
+
process.exit(2);
|
|
1325
|
+
}
|
|
1326
|
+
try {
|
|
1327
|
+
runUniversalGovernanceGate({
|
|
1328
|
+
sessionId,
|
|
1329
|
+
sourceRuntime: 'claude-code',
|
|
1330
|
+
surface: 'claude-pre-tool-gate',
|
|
1331
|
+
text: [unionText, JSON.stringify(toolInput || {})].join('\n').slice(0, 8000),
|
|
1332
|
+
action: cmd,
|
|
1333
|
+
toolName,
|
|
1334
|
+
filePath,
|
|
1335
|
+
isDeploy: Boolean(deployMatched),
|
|
1336
|
+
isMutation: toolName !== 'Bash',
|
|
1337
|
+
loadedSkills: skillGate.loadedSkills,
|
|
1338
|
+
evidence: { lensCount, hasVerify, hasCognition, hasSubstrateCite },
|
|
1339
|
+
});
|
|
1340
|
+
} catch (err) {
|
|
1341
|
+
audit('block-universal-governance', `${err instanceof Error ? err.message : String(err)}`.slice(0, 500));
|
|
1342
|
+
emitBlock(err instanceof Error ? err.message : String(err), { source: 'pre-tool/universal-governance', tool: toolName, lensCount, requiredLenses: REQUIRED_LENSES });
|
|
1343
|
+
process.exit(2);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1290
1346
|
function pushDecision(decision, reasonText) {
|
|
1291
1347
|
pushCognitionEvent({
|
|
1292
1348
|
sessionId,
|
|
@@ -1440,6 +1496,7 @@ if (deployMatched) {
|
|
|
1440
1496
|
// reads the same file and computes the same HMAC.
|
|
1441
1497
|
try {
|
|
1442
1498
|
const justificationPath = `${HOME}/.claude/.aria-deploy-justification.json`;
|
|
1499
|
+
const runtimeAgnosticJustificationPath = `${HOME}/.aria/.aria-deploy-justification.json`;
|
|
1443
1500
|
const secretPath = `${HOME}/.claude/.aria-gate-secret`;
|
|
1444
1501
|
|
|
1445
1502
|
// Lazy-generate per-installation secret if absent.
|
|
@@ -1481,6 +1538,8 @@ if (deployMatched) {
|
|
|
1481
1538
|
signatureAlgo: 'HMAC-SHA256',
|
|
1482
1539
|
};
|
|
1483
1540
|
writeFileSync(justificationPath, JSON.stringify(justification, null, 2));
|
|
1541
|
+
mkdirSync(dirname(runtimeAgnosticJustificationPath), { recursive: true });
|
|
1542
|
+
writeFileSync(runtimeAgnosticJustificationPath, JSON.stringify(justification, null, 2), { mode: 0o600 });
|
|
1484
1543
|
} catch (writeErr) {
|
|
1485
1544
|
// Write failure is non-fatal for the gate decision (the gate itself
|
|
1486
1545
|
// is the structural enforcement); log loudly per
|
|
@@ -2158,7 +2217,7 @@ Claude must either: (a) reframe action to fit allowedActions, OR (b) emit [PLAN_
|
|
|
2158
2217
|
// every tool when substrate is down would brick the orchestrator, but the
|
|
2159
2218
|
// failure is logged for telemetry.
|
|
2160
2219
|
try {
|
|
2161
|
-
const { HTTPHarnessClient } = await import('@
|
|
2220
|
+
const { HTTPHarnessClient } = await import('@aria_asi/harness-http-client');
|
|
2162
2221
|
const { readFileSync: __fsRead, existsSync: __fsExists } = await import('node:fs');
|
|
2163
2222
|
const { homedir: __homedir } = await import('node:os');
|
|
2164
2223
|
const __home = __homedir();
|
package/hooks/aria-stop-gate.mjs
CHANGED
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
|
|
50
50
|
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
51
51
|
import { dirname } from 'node:path';
|
|
52
|
+
import { spawnSync } from 'node:child_process';
|
|
52
53
|
import { appendGateAudit } from './lib/gate-audit.mjs';
|
|
53
54
|
import {
|
|
54
55
|
ALL_LENS_NAMES,
|
|
@@ -58,6 +59,8 @@ import {
|
|
|
58
59
|
} from './lib/canonical-lenses.mjs';
|
|
59
60
|
import { registerGateBlock } from './lib/gate-loop-state.mjs';
|
|
60
61
|
import { collectTurnWindowFromMessages } from './lib/hook-message-window.mjs';
|
|
62
|
+
import { analyzeDomainOutputQuality } from './lib/domain-output-quality.mjs';
|
|
63
|
+
import { evaluateSkillGate, formatSkillGateBlock } from './lib/skill-autoload-gate.mjs';
|
|
61
64
|
|
|
62
65
|
const HOME = process.env.HOME || '/tmp';
|
|
63
66
|
const RUNTIME_BASE_URL =
|
|
@@ -67,6 +70,24 @@ const LOG = `${HOME}/.claude/aria-stop-gate.log`;
|
|
|
67
70
|
const AUDIT_PATH = `${HOME}/.claude/aria-stop-gate-audit.jsonl`;
|
|
68
71
|
const GATE_LOOP_STATE_PATH = `${HOME}/.claude/.aria-gate-loop-state.json`;
|
|
69
72
|
const MIZAN_RECEIPT_DIR = `${HOME}/.claude/.aria-mizan-receipts`;
|
|
73
|
+
const GOVERNANCE_GATE_PATH = `${HOME}/.aria/bin/aria-governance-gate`;
|
|
74
|
+
|
|
75
|
+
function runUniversalGovernanceGate(payload) {
|
|
76
|
+
if (!existsSync(GOVERNANCE_GATE_PATH)) return null;
|
|
77
|
+
const child = spawnSync(GOVERNANCE_GATE_PATH, {
|
|
78
|
+
input: `${JSON.stringify(payload)}\n`,
|
|
79
|
+
encoding: 'utf8',
|
|
80
|
+
maxBuffer: 1024 * 1024,
|
|
81
|
+
});
|
|
82
|
+
const stdout = String(child.stdout || '').trim();
|
|
83
|
+
let result = null;
|
|
84
|
+
try { result = stdout ? JSON.parse(stdout) : null; } catch {}
|
|
85
|
+
if (child.status !== 0 || result?.ok === false || result?.decision === 'block') {
|
|
86
|
+
const reason = stdout || child.stderr || 'aria-governance-gate blocked this output.';
|
|
87
|
+
throw new Error(`=== ARIA UNIVERSAL GOVERNANCE GATE BLOCK ===\n\n${reason}`);
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
70
91
|
|
|
71
92
|
// SDK loader — bundled at ~/.aria/sdk by `aria connect`, with client-local
|
|
72
93
|
// fallbacks preserved for resilience.
|
|
@@ -619,6 +640,48 @@ if (!triggered) {
|
|
|
619
640
|
process.exit(0);
|
|
620
641
|
}
|
|
621
642
|
|
|
643
|
+
const stopSkillGate = evaluateSkillGate({
|
|
644
|
+
sessionId: event.session_id || 'claude-code',
|
|
645
|
+
surface: 'claude-stop-gate',
|
|
646
|
+
text: [JSON.stringify(event.messages || []), lastUserMessage, assistantText].join('\n'),
|
|
647
|
+
isOutputCloseout: true,
|
|
648
|
+
autoLoadAvailable: false,
|
|
649
|
+
});
|
|
650
|
+
if (!stopSkillGate.ok) {
|
|
651
|
+
audit('block-missing-skill-receipt', `missing=${stopSkillGate.missingSkills.join(',')} chars=${assistantText.length}`);
|
|
652
|
+
const reason = withLoopDirective(buildForceReauthorReason({
|
|
653
|
+
source: 'stop/skill-autoload',
|
|
654
|
+
reason: formatSkillGateBlock(stopSkillGate),
|
|
655
|
+
violations: [`Missing skill receipts: ${stopSkillGate.missingSkills.join(', ')}`],
|
|
656
|
+
lensCount: 0,
|
|
657
|
+
requiredLenses: REQUIRED_LENSES,
|
|
658
|
+
}), `stop:skill-autoload:${stopSkillGate.missingSkills.join(',')}`, gateSessionId);
|
|
659
|
+
console.log(JSON.stringify({ decision: 'block', reason }));
|
|
660
|
+
process.exit(2);
|
|
661
|
+
}
|
|
662
|
+
try {
|
|
663
|
+
runUniversalGovernanceGate({
|
|
664
|
+
sessionId: event.session_id || 'claude-code',
|
|
665
|
+
sourceRuntime: 'claude-code',
|
|
666
|
+
surface: 'claude-stop-gate',
|
|
667
|
+
text: assistantText.slice(0, 8000),
|
|
668
|
+
isOutputCloseout: true,
|
|
669
|
+
loadedSkills: stopSkillGate.loadedSkills,
|
|
670
|
+
evidence: { chars: assistantText.length, hasDecisionSignal, isLong },
|
|
671
|
+
});
|
|
672
|
+
} catch (err) {
|
|
673
|
+
audit('block-universal-governance', `${err instanceof Error ? err.message : String(err)}`.slice(0, 500));
|
|
674
|
+
const reason = withLoopDirective(buildForceReauthorReason({
|
|
675
|
+
source: 'stop/universal-governance',
|
|
676
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
677
|
+
violations: ['Universal governance gate blocked this output.'],
|
|
678
|
+
lensCount: 0,
|
|
679
|
+
requiredLenses: REQUIRED_LENSES,
|
|
680
|
+
}), 'stop:universal-governance', gateSessionId);
|
|
681
|
+
console.log(JSON.stringify({ decision: 'block', reason }));
|
|
682
|
+
process.exit(2);
|
|
683
|
+
}
|
|
684
|
+
|
|
622
685
|
// Non-trivial response — require substantive cognition.
|
|
623
686
|
const cog = detectCognitionLenses(assistantText);
|
|
624
687
|
|
|
@@ -1245,7 +1308,7 @@ if (cog.count >= REQUIRED_LENSES) {
|
|
|
1245
1308
|
}
|
|
1246
1309
|
|
|
1247
1310
|
// Block decision: any of (validateOutput severity=block) OR (>=2 drift hits) OR
|
|
1248
|
-
// (>=1 code-quality hit) OR (open discovery in ledger) → block emit.
|
|
1311
|
+
// (>=1 code/domain-quality hit) OR (open discovery in ledger) → block emit.
|
|
1249
1312
|
// Aria enforcement #46 (compelled reflection): severity=warn ALSO blocks but
|
|
1250
1313
|
// with a different reason — emit must include explicit reflection on what
|
|
1251
1314
|
// triggered the warn before re-emit. Warn is not "soft pass" anymore;
|
|
@@ -1255,6 +1318,8 @@ if (cog.count >= REQUIRED_LENSES) {
|
|
|
1255
1318
|
const mizanWarnReflectionRequired = mizanVerdict && mizanVerdict.severity === 'warn';
|
|
1256
1319
|
const driftBlock = driftHits.length >= 2;
|
|
1257
1320
|
const codeBlock = codeQualityHits.length >= 1;
|
|
1321
|
+
const domainQuality = analyzeDomainOutputQuality(assistantText, { codeBlocks });
|
|
1322
|
+
const domainBlock = domainQuality.blockers.length >= 1;
|
|
1258
1323
|
|
|
1259
1324
|
// Reflection-already-present check: if the assistant text already contains
|
|
1260
1325
|
// an explicit <reflection>...</reflection> block OR a "reflection:" line
|
|
@@ -1374,7 +1439,7 @@ if (cog.count >= REQUIRED_LENSES) {
|
|
|
1374
1439
|
let substrateViolations = [];
|
|
1375
1440
|
let substrateGateTriggers = [];
|
|
1376
1441
|
try {
|
|
1377
|
-
const { HTTPHarnessClient } = await import('@
|
|
1442
|
+
const { HTTPHarnessClient } = await import('@aria_asi/harness-http-client');
|
|
1378
1443
|
const tokenPath = `${HOME}/.aria/owner-token`;
|
|
1379
1444
|
// Tier-aware resolution: ARIA_HARNESS_TOKEN env first (both tiers).
|
|
1380
1445
|
// ONLY on owner tier, fall back to master/api-key env or owner-token
|
|
@@ -1413,13 +1478,14 @@ if (cog.count >= REQUIRED_LENSES) {
|
|
|
1413
1478
|
}
|
|
1414
1479
|
|
|
1415
1480
|
const implCouplingBlock = implCouplingHits.length > 0;
|
|
1416
|
-
if (mizanBlock || driftBlock || codeBlock || discoveryBlock || compelReflection || phaseReportMissing || substrateBlock || implCouplingBlock) {
|
|
1481
|
+
if (mizanBlock || driftBlock || codeBlock || domainBlock || discoveryBlock || compelReflection || phaseReportMissing || substrateBlock || implCouplingBlock) {
|
|
1417
1482
|
const violations = [];
|
|
1418
1483
|
if (mizanBlock) violations.push(`Mizan: ${(mizanVerdict.violations || []).join(', ')}`);
|
|
1419
1484
|
if (implCouplingBlock) violations.push(`Cognition impl-coupling (#88): ${implCouplingHits.join(' | ')}. Each canonical lens in cognition must dictate a specific implementation choice (file_path:line_range pair tied to a decision). Re-emit cognition that names file paths + line ranges + decision text per lens, OR a verify/fixing block where lenses cite specific artifact changes.`);
|
|
1420
1485
|
if (compelReflection) violations.push(`Mizan severity=warn — compelled reflection required (per Aria enforcement #46). Triggers: ${(mizanVerdict.gateTriggers || mizanVerdict.violations || ['unspecified']).join(', ')}. Re-emit with an explicit <reflection>...</reflection> block (or 'reflection:' line) addressing what triggered the warn and why your re-draft handles it. Reflection is NOT lens-cognition repeated — it's a focused self-audit on the specific Mizan triggers above.`);
|
|
1421
1486
|
if (driftBlock) violations.push(`Drift triggers (${driftHits.length}): ${driftHits.map((h) => `"${h.trigger}" → ${h.memory}`).join(' | ')}`);
|
|
1422
1487
|
if (codeBlock) violations.push(`Code quality: ${codeQualityHits.join('; ')}`);
|
|
1488
|
+
if (domainBlock) violations.push(`Domain output QA (${domainQuality.domains.join(', ') || 'general'}): ${domainQuality.blockers.join('; ')}. Rewrite with the missing domain-specific safeguards instead of generic prose.`);
|
|
1423
1489
|
if (discoveryBlock) violations.push(`Discovery-binding ledger has ${ledgerOpenCount} OPEN discoveries (per ${docRef('feedback_no_flag_without_fix.md', 'atomic-discovery-rule')}, discoveries are atomic with their fixes — fix in the same turn or create a TaskCreate before continuing). Recent open: ${ledgerOpenSamples.map((s) => `"${s.slice(0, 80)}"`).join(' | ')}. Resolve each by either (a) fixing it inline in this turn, or (b) creating a TaskCreate with the discovery's full context (file path, line number, what's broken, why), then editing ${LEDGER_PATH} to set status=resolved.`);
|
|
1424
1490
|
if (phaseReportMissing) {
|
|
1425
1491
|
const phaseList = (activePlan?.phases || []).map((p) => `${p.id}:${p.summary?.slice(0, 60) || ''}`).join(' | ');
|
|
@@ -508,6 +508,49 @@
|
|
|
508
508
|
"counter_action": "Create or reuse one turn substrate object containing embedding, perturb snapshot, ProjectAllDomains result, awakenAria Garden block, DeepSoul/Noor/shards/Mizan signals; pass it downstream.",
|
|
509
509
|
"message": "Duplicate projection path detected. Replace with one per-turn cognition packet and shared consumption."
|
|
510
510
|
},
|
|
511
|
+
{
|
|
512
|
+
"trigger_id": "premature_task_closeout",
|
|
513
|
+
"trigger": "(?:done|complete|completed|ready|verified|fixed).{0,120}(?:but|except|caveat|remaining|not yet|still|separate|later|blocked|skipped)",
|
|
514
|
+
"rx": "(?:done|complete|completed|ready|verified|fixed).{0,120}(?:but|except|caveat|remaining|not yet|still|separate|later|blocked|skipped)",
|
|
515
|
+
"doctrine": "memory:feedback_no_premature_task_closeout.md",
|
|
516
|
+
"memory": "feedback_no_premature_task_closeout.md",
|
|
517
|
+
"severity": "block",
|
|
518
|
+
"teaching": "A task is not complete while material blockers remain. Completion claims must match the verified scope.",
|
|
519
|
+
"counter_action": "Do not close the task. Fix the blocker now, or create an owner-visible durable task with the exact failing surface and verification gap.",
|
|
520
|
+
"message": "Premature closeout detected: completion language coexists with unresolved blockers. Fix or durably track before emitting."
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
"trigger_id": "narrow_e2e_overclaim",
|
|
524
|
+
"trigger": "(?:production-ready|ready for production|works in general|client packages?|npm packages?|SDKs?|runtimes?|harnesses?).{0,220}(?:deal|single flow|one flow|widget|one service|specific path|covered flow)",
|
|
525
|
+
"rx": "(?:production-ready|ready for production|works in general|client packages?|npm packages?|SDKs?|runtimes?|harnesses?).{0,220}(?:deal|single flow|one flow|widget|one service|specific path|covered flow)",
|
|
526
|
+
"doctrine": "memory:feedback_narrow_e2e_overclaim.md",
|
|
527
|
+
"memory": "feedback_narrow_e2e_overclaim.md",
|
|
528
|
+
"severity": "block",
|
|
529
|
+
"teaching": "A narrow e2e does not prove general production readiness for SDKs, npm packages, runtimes, clients, or harnesses.",
|
|
530
|
+
"counter_action": "Run the general readiness matrix or explicitly limit the claim to the verified surface. Do not imply general readiness from one app flow.",
|
|
531
|
+
"message": "Narrow proof overclaim detected. General readiness requires the client/package/runtime/harness matrix."
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
"trigger_id": "advisory_gate_not_gate",
|
|
535
|
+
"trigger": "(?:non-blocking|warn(?:ing)? only|advisory|falls? through|fail open|soft fail|log(?:ged)? and continue|quality gate warning)",
|
|
536
|
+
"rx": "(?:non-blocking|warn(?:ing)? only|advisory|falls? through|fail open|soft fail|log(?:ged)? and continue|quality gate warning)",
|
|
537
|
+
"doctrine": "memory:feedback_advisory_gate_is_not_gate.md",
|
|
538
|
+
"memory": "feedback_advisory_gate_is_not_gate.md",
|
|
539
|
+
"severity": "block",
|
|
540
|
+
"teaching": "A gate that only warns or falls through is not a gate. Enforcement must fail closed where quality or doctrine is required.",
|
|
541
|
+
"counter_action": "Convert advisory paths to blocking errors, add a self-test that proves rejection, and install the hardened gate into each consumer surface.",
|
|
542
|
+
"message": "Advisory gate bypass detected. Convert to fail-closed enforcement and prove it with a blocking self-test."
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
"trigger_id": "start_new_session_as_gate_fix",
|
|
546
|
+
"trigger": "(?:start|open|begin).{0,40}(?:new|fresh).{0,30}session.{0,120}(?:fix|gate|harness|enforcement)",
|
|
547
|
+
"rx": "(?:start|open|begin).{0,40}(?:new|fresh).{0,30}session.{0,120}(?:fix|gate|harness|enforcement)",
|
|
548
|
+
"doctrine": "memory:feedback_advisory_gate_is_not_gate.md",
|
|
549
|
+
"memory": "feedback_advisory_gate_is_not_gate.md",
|
|
550
|
+
"severity": "block",
|
|
551
|
+
"teaching": "A new session is not an enforcement fix. It only reloads whatever gate contract exists; broken gates remain broken.",
|
|
552
|
+
"counter_action": "Fix the runtime/plugin/hook contract, reinstall it, and run a live bad-action/bad-output self-test that proves blocking."
|
|
553
|
+
},
|
|
511
554
|
{
|
|
512
555
|
"trigger_id": "registry_image_drift",
|
|
513
556
|
"trigger": "image.?pull|ErrImageNeverPull|ImagePullBackOff|registry gc|image.*missing|image.*not.*found",
|
|
@@ -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 @
|
|
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
|
|
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(
|
|
176
|
-
const filePath = toolName !== 'Bash' ? String(
|
|
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
|
|
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`);
|