@aria_asi/cli 0.2.38 → 0.2.40
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/README.md +140 -0
- package/bin/aria.js +8 -4
- package/dist/aria-connector/src/auth.d.ts +1 -0
- package/dist/aria-connector/src/auth.d.ts.map +1 -1
- package/dist/aria-connector/src/auth.js +26 -1
- package/dist/aria-connector/src/auth.js.map +1 -1
- package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -1
- package/dist/aria-connector/src/connectors/codex.js +52 -3
- 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 +41 -1
- package/dist/aria-connector/src/setup-wizard.js.map +1 -1
- package/dist/aria-connector/src/types.d.ts +6 -0
- package/dist/aria-connector/src/types.d.ts.map +1 -1
- package/dist/assets/hooks/aria-pre-tool-gate.mjs +40 -12
- package/dist/cli-0.2.38.tgz +0 -0
- package/dist/runtime/coach-kernel.mjs +59 -4
- package/dist/runtime/gated-ledger.mjs +237 -0
- package/dist/runtime/hooks/aria-pre-tool-gate.mjs +40 -12
- package/dist/runtime/manifest.json +1 -1
- package/dist/runtime/quality-enforcer.mjs +257 -0
- package/dist/runtime/sdk/BUNDLED.json +1 -1
- package/dist/runtime/service.mjs +119 -0
- package/dist/sdk/BUNDLED.json +1 -1
- package/hooks/aria-pre-tool-gate.mjs +40 -12
- package/package.json +1 -1
- package/runtime-src/coach-kernel.mjs +59 -4
- package/runtime-src/gated-ledger.mjs +237 -0
- package/runtime-src/quality-enforcer.mjs +257 -0
- package/runtime-src/service.mjs +119 -0
- package/scripts/install-client.sh +32 -2
- package/src/auth.ts +25 -1
- package/src/connectors/codex.ts +52 -3
- package/src/setup-wizard.ts +43 -1
- package/src/types.ts +6 -0
package/dist/runtime/service.mjs
CHANGED
|
@@ -42,8 +42,11 @@ import {
|
|
|
42
42
|
formatCoachClientBlock,
|
|
43
43
|
readCoachState,
|
|
44
44
|
recordCoachPhase,
|
|
45
|
+
triggerMissingSkills,
|
|
45
46
|
} from './coach-kernel.mjs';
|
|
46
47
|
import { resolveAriaAuthToken } from './auth-token.mjs';
|
|
48
|
+
import { check, enforceWithRecovery as enforceQualityWithRecovery } from './quality-enforcer.mjs';
|
|
49
|
+
import { enforceGates } from './gated-ledger.mjs';
|
|
47
50
|
|
|
48
51
|
const require = createRequire(import.meta.url);
|
|
49
52
|
const { runFullChain } = require('./vendor/aria-gate-runtime/index.js');
|
|
@@ -3726,6 +3729,15 @@ async function evaluateProviderCandidate(req, body, client, apiKey, turn, provid
|
|
|
3726
3729
|
};
|
|
3727
3730
|
}
|
|
3728
3731
|
|
|
3732
|
+
function deriveEffectiveKernel(turn, body) {
|
|
3733
|
+
const msg = body?.message || body?.prompt || body?.input || '';
|
|
3734
|
+
if (turn?.turnClass === 'repair' || /repair|fix|debug|broken|bug|error|failing|crash|recover/i.test(msg)) return 'repair';
|
|
3735
|
+
if (turn?.turnClass === 'architect' || /architecture|design|system|pipeline|tradeoff|ADR/i.test(msg)) return 'architect';
|
|
3736
|
+
if (turn?.turnClass === 'action' || /deploy|execute|run|build|ship|rollout|restart|push/i.test(msg)) return 'action';
|
|
3737
|
+
if (turn?.turnClass === 'research' || /research|search|find|recall|retrieve|look.up|investigate/i.test(msg)) return 'research';
|
|
3738
|
+
return 'emotional_presence';
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3729
3741
|
async function handleProviderProxy(req, body, client, providerStyle, options = {}) {
|
|
3730
3742
|
const apiKey = resolveApiKey(req, body, options);
|
|
3731
3743
|
const startedAt = Date.now();
|
|
@@ -3819,6 +3831,44 @@ async function handleProviderProxy(req, body, client, providerStyle, options = {
|
|
|
3819
3831
|
],
|
|
3820
3832
|
});
|
|
3821
3833
|
coachRecords.push(preGenerationCoach);
|
|
3834
|
+
// ── Coach auto-trigger: load missing skills instead of blocking ──
|
|
3835
|
+
if (preGenerationCoach.decision === 'auto_trigger_skills' && turn.missingSkillIds?.length > 0) {
|
|
3836
|
+
const skillResult = await triggerMissingSkills(
|
|
3837
|
+
turn.missingSkillIds,
|
|
3838
|
+
SKILL_SEARCH_ROOTS,
|
|
3839
|
+
);
|
|
3840
|
+
const loadedCoach = recordRuntimeCoachPhase({
|
|
3841
|
+
phase: 'pre_generation',
|
|
3842
|
+
body,
|
|
3843
|
+
turn: { ...turn, loadedSkillIds: [...(turn.loadedSkillIds || []), ...skillResult.loaded], missingSkillIds: skillResult.stillMissing },
|
|
3844
|
+
providerStyle,
|
|
3845
|
+
providerPlan,
|
|
3846
|
+
text: turn.userMessage,
|
|
3847
|
+
evidenceRefs: [`skills_loaded:${skillResult.loaded.length}`, `skills_still_missing:${skillResult.stillMissing.length}`],
|
|
3848
|
+
metadata: { autoLoadedSkills: skillResult.loaded },
|
|
3849
|
+
});
|
|
3850
|
+
coachRecords.push(loadedCoach);
|
|
3851
|
+
if (loadedCoach.decision === 'hard_block') {
|
|
3852
|
+
const refusal = formatCoachClientBlock(loadedCoach);
|
|
3853
|
+
const autoBlockRecord = appendManagedRuntimeLedger(buildManagedRuntimeLedgerRecord({
|
|
3854
|
+
phase: 'pre_generation_skills_block',
|
|
3855
|
+
body, turn, providerStyle, providerPlan,
|
|
3856
|
+
releaseDecision: 'hard_block',
|
|
3857
|
+
blockers: loadedCoach.reasons,
|
|
3858
|
+
evidenceRefs: coachRecordRefs(coachRecords),
|
|
3859
|
+
}));
|
|
3860
|
+
ledgerRecords.push({ phase: 'pre_generation_skills_block', ...autoBlockRecord });
|
|
3861
|
+
return providerStyle === 'anthropic'
|
|
3862
|
+
? anthropicResponseEnvelope(refusal, { model: body.model || 'aria-runtime', finishReason: 'end_turn' }, {}, body?.ariaDebug === true)
|
|
3863
|
+
: openAiResponseEnvelope(body, refusal, { model: body.model || 'aria-runtime', finishReason: 'stop', usage: null }, {});
|
|
3864
|
+
}
|
|
3865
|
+
// Skills loaded — update turn for the rest of the pipeline
|
|
3866
|
+
turn.loadedSkillIds = [...(turn.loadedSkillIds || []), ...skillResult.loaded];
|
|
3867
|
+
turn.missingSkillIds = skillResult.stillMissing;
|
|
3868
|
+
if (skillResult.loadedBodies.length > 0) {
|
|
3869
|
+
turn.skillBodies = [...(turn.skillBodies || []), ...skillResult.loadedBodies.map((s) => s.body)];
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3822
3872
|
if (preGenerationCoach.decision === 'hard_block') {
|
|
3823
3873
|
const refusal = formatCoachClientBlock(preGenerationCoach);
|
|
3824
3874
|
const preBlockRecord = appendManagedRuntimeLedger(buildManagedRuntimeLedgerRecord({
|
|
@@ -3877,6 +3927,17 @@ async function handleProviderProxy(req, body, client, providerStyle, options = {
|
|
|
3877
3927
|
ledgerRecords.push({ phase: 'provider_call_failed', ...failureRecord });
|
|
3878
3928
|
throw error;
|
|
3879
3929
|
}
|
|
3930
|
+
|
|
3931
|
+
const providerQualityResult = await enforceQualityWithRecovery(
|
|
3932
|
+
providerMeta.text || '',
|
|
3933
|
+
deriveEffectiveKernel(turn, body),
|
|
3934
|
+
{ sessionId: turn.sessionId || body.sessionId },
|
|
3935
|
+
);
|
|
3936
|
+
if (providerQualityResult.enforced) {
|
|
3937
|
+
providerMeta.text = providerQualityResult.finalText;
|
|
3938
|
+
providerMeta.qualityEnforced = true;
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3880
3941
|
recordProviderUsage(body, turn, providerMeta);
|
|
3881
3942
|
let hardCoachBlock = null;
|
|
3882
3943
|
let evaluation = await evaluateProviderCandidate(req, body, client, apiKey, turn, providerStyle, providerMeta, providerMeta.text || '');
|
|
@@ -4125,11 +4186,55 @@ async function handleProviderProxy(req, body, client, providerStyle, options = {
|
|
|
4125
4186
|
records: coachRecords,
|
|
4126
4187
|
},
|
|
4127
4188
|
};
|
|
4189
|
+
const finalQualityResult = await enforceQualityWithRecovery(
|
|
4190
|
+
finalText,
|
|
4191
|
+
deriveEffectiveKernel(turn, body),
|
|
4192
|
+
{ sessionId: turn.sessionId || body.sessionId },
|
|
4193
|
+
);
|
|
4194
|
+
if (finalQualityResult.enforced) {
|
|
4195
|
+
finalText = finalQualityResult.finalText;
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4198
|
+
// ── Gated Ledger: final enforcement before release ──
|
|
4199
|
+
const gated = await enforceGates(finalText, {
|
|
4200
|
+
kernel: deriveEffectiveKernel(turn, body),
|
|
4201
|
+
sessionId: turn.sessionId || body.sessionId,
|
|
4202
|
+
});
|
|
4203
|
+
if (gated.enforced) {
|
|
4204
|
+
extra.gatedLedger = {
|
|
4205
|
+
enforced: true,
|
|
4206
|
+
gates: gated.gates,
|
|
4207
|
+
doctrineTriggers: gated.doctrineTriggers,
|
|
4208
|
+
recordId: gated.record.recordId,
|
|
4209
|
+
};
|
|
4210
|
+
}
|
|
4211
|
+
finalText = gated.finalText;
|
|
4212
|
+
|
|
4128
4213
|
return providerStyle === 'anthropic'
|
|
4129
4214
|
? anthropicResponseEnvelope(finalText, providerMeta, extra, body?.ariaDebug === true)
|
|
4130
4215
|
: openAiResponseEnvelope(body, finalText, providerMeta, extra);
|
|
4131
4216
|
}
|
|
4132
4217
|
|
|
4218
|
+
function extractProviderResponseText(response) {
|
|
4219
|
+
if (!response || typeof response !== 'object') return null;
|
|
4220
|
+
const choices = response.choices;
|
|
4221
|
+
if (Array.isArray(choices) && choices.length > 0) {
|
|
4222
|
+
const content = choices[0]?.message?.content;
|
|
4223
|
+
return typeof content === 'string' ? content : null;
|
|
4224
|
+
}
|
|
4225
|
+
return null;
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
function extractAnthropicResponseText(response) {
|
|
4229
|
+
if (!response || typeof response !== 'object') return null;
|
|
4230
|
+
const content = response.content;
|
|
4231
|
+
if (Array.isArray(content)) {
|
|
4232
|
+
return content.map(c => (c && c.text ? c.text : '')).join(' ').trim() || null;
|
|
4233
|
+
}
|
|
4234
|
+
if (typeof content === 'string') return content;
|
|
4235
|
+
return null;
|
|
4236
|
+
}
|
|
4237
|
+
|
|
4133
4238
|
async function handleForgeSynthesis(req, body, client) {
|
|
4134
4239
|
const apiKey = resolveApiKey(req, body);
|
|
4135
4240
|
const startedAt = Date.now();
|
|
@@ -5338,6 +5443,13 @@ async function handleRoute(req, res) {
|
|
|
5338
5443
|
|
|
5339
5444
|
if (providerPath === '/v1/chat/completions') {
|
|
5340
5445
|
const response = await handleProviderProxy(req, body, client, 'openai', ariaAuthOptions);
|
|
5446
|
+
const responseText = extractProviderResponseText(response);
|
|
5447
|
+
if (responseText) {
|
|
5448
|
+
const qScan = check(responseText);
|
|
5449
|
+
if (!qScan.allowed) {
|
|
5450
|
+
console.warn(`[quality-enforcer] Gate labels detected in OpenAI response after handleProviderProxy. ${qScan.reasons.join(', ')}`);
|
|
5451
|
+
}
|
|
5452
|
+
}
|
|
5341
5453
|
return json(res, 200, response);
|
|
5342
5454
|
}
|
|
5343
5455
|
|
|
@@ -5349,6 +5461,13 @@ async function handleRoute(req, res) {
|
|
|
5349
5461
|
|
|
5350
5462
|
if (providerPath === '/v1/messages') {
|
|
5351
5463
|
const response = await handleProviderProxy(req, body, client, 'anthropic', ariaAuthOptions);
|
|
5464
|
+
const responseText = extractAnthropicResponseText(response);
|
|
5465
|
+
if (responseText) {
|
|
5466
|
+
const qScan = check(responseText);
|
|
5467
|
+
if (!qScan.allowed) {
|
|
5468
|
+
console.warn(`[quality-enforcer] Gate labels detected in Anthropic response after handleProviderProxy. ${qScan.reasons.join(', ')}`);
|
|
5469
|
+
}
|
|
5470
|
+
}
|
|
5352
5471
|
return json(res, 200, response);
|
|
5353
5472
|
}
|
|
5354
5473
|
|
package/dist/sdk/BUNDLED.json
CHANGED
|
@@ -73,17 +73,8 @@ function runUniversalGovernanceGate(payload) {
|
|
|
73
73
|
try { result = stdout ? JSON.parse(stdout) : null; } catch {}
|
|
74
74
|
if (child.status !== 0 || result?.ok === false || result?.decision === 'block') {
|
|
75
75
|
const reason = stdout || child.stderr || 'aria-governance-gate blocked this action.';
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
'',
|
|
79
|
-
reason,
|
|
80
|
-
'',
|
|
81
|
-
'Recovery contract:',
|
|
82
|
-
'1. Do not retry the same blocked action unchanged.',
|
|
83
|
-
'2. Load or apply the doctrine/skill named by the governance output.',
|
|
84
|
-
'3. Re-write the action with <applied_cognition> and concrete proof.',
|
|
85
|
-
'4. Re-test by submitting the revised action through this same gate.',
|
|
86
|
-
].join('\n'));
|
|
76
|
+
process.stderr.write(`[aria-governance:block] ${reason.slice(0, 500)}\n`);
|
|
77
|
+
return { decision: 'block', ok: false, reason, governanceMode: 'block', raw: result };
|
|
87
78
|
}
|
|
88
79
|
if (result?.decision === 'warn' || result?.governanceMode === 'recovery-required' || result?.governanceMode === 'architectural-intervention-required') {
|
|
89
80
|
process.stderr.write(`[aria-governance:${result.governanceMode || 'recovery-required'}] ${JSON.stringify(result)}\n`);
|
|
@@ -1396,7 +1387,7 @@ if (!skillGate.ok && !skillGate.redirectOnly) {
|
|
|
1396
1387
|
process.exit(2);
|
|
1397
1388
|
}
|
|
1398
1389
|
try {
|
|
1399
|
-
runUniversalGovernanceGate({
|
|
1390
|
+
const govGateResult = runUniversalGovernanceGate({
|
|
1400
1391
|
sessionId,
|
|
1401
1392
|
sourceRuntime: 'claude-code',
|
|
1402
1393
|
surface: 'claude-pre-tool-gate',
|
|
@@ -1409,6 +1400,43 @@ try {
|
|
|
1409
1400
|
loadedSkills: skillGate.loadedSkills,
|
|
1410
1401
|
evidence: { lensCount, hasVerify, hasCognition, hasSubstrateCite },
|
|
1411
1402
|
});
|
|
1403
|
+
if (govGateResult?.decision === 'block') {
|
|
1404
|
+
audit('signal-gov-gate-block', `reason=${(govGateResult.reason || '').slice(0, 120)}`);
|
|
1405
|
+
const _cmd = String(toolInput?.command || '');
|
|
1406
|
+
try {
|
|
1407
|
+
const _coachUrl = `${HOME}/.aria/runtime/runtime.env`;
|
|
1408
|
+
const _coachBase = existsSync(_coachUrl)
|
|
1409
|
+
? String(readFileSync(_coachUrl, 'utf8')).match(/ARIA_RUNTIME_URL=(http:\/\/[^ \n]+)/)?.[1] || 'http://127.0.0.1:4319'
|
|
1410
|
+
: 'http://127.0.0.1:4319';
|
|
1411
|
+
const _coachToken = (() => {
|
|
1412
|
+
const tp = `${HOME}/.aria/owner-token`;
|
|
1413
|
+
if (existsSync(tp)) return readFileSync(tp, 'utf8').trim();
|
|
1414
|
+
return process.env.ARIA_API_KEY || process.env.ARIA_MASTER_TOKEN || '';
|
|
1415
|
+
})();
|
|
1416
|
+
const _coachHeaders = { 'Content-Type': 'application/json' };
|
|
1417
|
+
if (_coachToken) _coachHeaders.Authorization = `Bearer ${_coachToken}`;
|
|
1418
|
+
const _coachResp = await fetch(`${_coachBase}/coach/phase`, {
|
|
1419
|
+
method: 'POST', headers: _coachHeaders,
|
|
1420
|
+
body: JSON.stringify({
|
|
1421
|
+
phase: 'pre_tool', requestId: `claude-gov-gate:${Date.now()}`,
|
|
1422
|
+
sessionId, surface: 'claude-hooks', lane: 'claude_native_hooks',
|
|
1423
|
+
action: deployMatched ? 'deploy' : '',
|
|
1424
|
+
text: _cmd.slice(0, 1000),
|
|
1425
|
+
metadata: { source: 'claude-governance-gate', toolName, governanceGateBlock: true },
|
|
1426
|
+
evidenceRefs: [{ kind: 'governance_gate', reason: govGateResult.reason }],
|
|
1427
|
+
}),
|
|
1428
|
+
signal: AbortSignal.timeout(2000),
|
|
1429
|
+
});
|
|
1430
|
+
if (_coachResp.ok) {
|
|
1431
|
+
const _coachBody = await _coachResp.json();
|
|
1432
|
+
if (_coachBody?.permitted === false) {
|
|
1433
|
+
audit('block-coach-after-gov-gate', `reasons=${(_coachBody.reasons||[]).join(',')}`);
|
|
1434
|
+
emitBlock(`Aria Coach blocked after governance gate signal: ${(_coachBody.reasons||['gate_violation']).join('; ')}`, { source: 'pre-tool/coach-after-gov-gate', tool: toolName });
|
|
1435
|
+
process.exit(2);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
} catch {}
|
|
1439
|
+
}
|
|
1412
1440
|
} catch (err) {
|
|
1413
1441
|
audit('block-universal-governance', `${err instanceof Error ? err.message : String(err)}`.slice(0, 500));
|
|
1414
1442
|
emitBlock(err instanceof Error ? err.message : String(err), { source: 'pre-tool/universal-governance', tool: toolName, lensCount, requiredLenses: REQUIRED_LENSES });
|
package/package.json
CHANGED
|
@@ -127,7 +127,7 @@ function inferRiskClass(highRisk, repairable) {
|
|
|
127
127
|
return 'low';
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
function decisionFromSignals(highRisk, repairable, warnings) {
|
|
130
|
+
function decisionFromSignals(highRisk, repairable, warnings, autoTrigger = []) {
|
|
131
131
|
if (highRisk.length > 0) {
|
|
132
132
|
return {
|
|
133
133
|
decision: 'hard_block',
|
|
@@ -136,6 +136,14 @@ function decisionFromSignals(highRisk, repairable, warnings) {
|
|
|
136
136
|
reasons: highRisk,
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
|
+
if (autoTrigger.length > 0) {
|
|
140
|
+
return {
|
|
141
|
+
decision: 'auto_trigger_skills',
|
|
142
|
+
permitted: false,
|
|
143
|
+
nextAction: 'auto_load_missing_skills_and_retry',
|
|
144
|
+
reasons: autoTrigger,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
139
147
|
if (repairable.length > 0) {
|
|
140
148
|
return {
|
|
141
149
|
decision: 'repair_once',
|
|
@@ -201,6 +209,7 @@ export function evaluateCoachEvent(event = {}) {
|
|
|
201
209
|
const highRisk = [];
|
|
202
210
|
const repairable = [];
|
|
203
211
|
const warnings = [];
|
|
212
|
+
const autoTrigger = [];
|
|
204
213
|
const action = String(normalized.action || '').toLowerCase();
|
|
205
214
|
|
|
206
215
|
if (!ALLOWED_PHASES.has(normalized.phase)) {
|
|
@@ -210,7 +219,7 @@ export function evaluateCoachEvent(event = {}) {
|
|
|
210
219
|
highRisk.push('secret_or_credential_exposure');
|
|
211
220
|
}
|
|
212
221
|
if (normalized.phase === 'pre_generation' && normalized.missing_skill_ids.length > 0) {
|
|
213
|
-
|
|
222
|
+
autoTrigger.push('required_skill_auto_load');
|
|
214
223
|
}
|
|
215
224
|
if (TOOL_PHASES.has(normalized.phase) || action) {
|
|
216
225
|
if ((action === 'delete' || DESTRUCTIVE_RX.test(text)) && normalized.metadata?.approved !== true) {
|
|
@@ -233,19 +242,20 @@ export function evaluateCoachEvent(event = {}) {
|
|
|
233
242
|
}
|
|
234
243
|
}
|
|
235
244
|
if (normalized.missing_skill_ids.length > 0 && !normalized.metadata?.skillsAdvisoryOnly) {
|
|
236
|
-
|
|
245
|
+
autoTrigger.push('required_skills_not_loaded');
|
|
237
246
|
}
|
|
238
247
|
if (normalized.lane.includes('unmanaged') || normalized.metadata?.complianceGuarantee === 'best_effort_only') {
|
|
239
248
|
warnings.push('unmanaged_direct_provider_best_effort_only');
|
|
240
249
|
}
|
|
241
250
|
|
|
242
|
-
const verdict = decisionFromSignals(highRisk, repairable, warnings);
|
|
251
|
+
const verdict = decisionFromSignals(highRisk, repairable, warnings, autoTrigger);
|
|
243
252
|
return {
|
|
244
253
|
...verdict,
|
|
245
254
|
riskClass: inferRiskClass(highRisk, repairable),
|
|
246
255
|
highRisk,
|
|
247
256
|
repairable,
|
|
248
257
|
warnings,
|
|
258
|
+
autoTrigger,
|
|
249
259
|
};
|
|
250
260
|
}
|
|
251
261
|
|
|
@@ -375,3 +385,48 @@ export function formatCoachClientBlock(recordOrResult = {}) {
|
|
|
375
385
|
`Next: ${next}`,
|
|
376
386
|
].join('\n');
|
|
377
387
|
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Auto-trigger skill loading when the Coach detects missing skills.
|
|
391
|
+
* Searches the skill registry for each missing skill ID and loads their
|
|
392
|
+
* body text into the turn context so the LLM can apply them.
|
|
393
|
+
*/
|
|
394
|
+
export async function triggerMissingSkills(missingSkillIds = [], skillSearchRoots = []) {
|
|
395
|
+
if (!missingSkillIds.length) return { loaded: [], stillMissing: [], loadedBodies: [] };
|
|
396
|
+
|
|
397
|
+
const loaded = [];
|
|
398
|
+
const stillMissing = [];
|
|
399
|
+
const loadedBodies = [];
|
|
400
|
+
|
|
401
|
+
const roots = skillSearchRoots.length > 0 ? skillSearchRoots : [
|
|
402
|
+
join(dirname(fileURLToPath(import.meta.url)), 'discipline', 'skills'),
|
|
403
|
+
join(process.env.HOME || '', '.aria', 'runtime', 'discipline', 'skills'),
|
|
404
|
+
join(process.env.HOME || '', '.claude', 'skills'),
|
|
405
|
+
join(process.env.HOME || '', '.agents', 'skills'),
|
|
406
|
+
];
|
|
407
|
+
|
|
408
|
+
for (const skillId of missingSkillIds) {
|
|
409
|
+
let found = false;
|
|
410
|
+
for (const root of roots) {
|
|
411
|
+
if (!existsSync(root)) continue;
|
|
412
|
+
try {
|
|
413
|
+
const entries = require('node:fs').readdirSync(root, { recursive: true, withFileTypes: true });
|
|
414
|
+
for (const entry of entries) {
|
|
415
|
+
if (!entry.isFile() || !entry.name.endsWith('.md') && !entry.name.endsWith('SKILL.md')) continue;
|
|
416
|
+
if (entry.name.toLowerCase().includes(skillId.toLowerCase()) ||
|
|
417
|
+
entry.parentPath?.toLowerCase().includes(skillId.toLowerCase())) {
|
|
418
|
+
const body = readFileSync(join(entry.parentPath || root, entry.name), 'utf8');
|
|
419
|
+
loaded.push(skillId);
|
|
420
|
+
loadedBodies.push({ skillId, path: join(entry.parentPath || root, entry.name), body: body.slice(0, 20000) });
|
|
421
|
+
found = true;
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
} catch {}
|
|
426
|
+
if (found) break;
|
|
427
|
+
}
|
|
428
|
+
if (!found) stillMissing.push(skillId);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return { loaded, stillMissing, loadedBodies };
|
|
432
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Gated Ledger — End-to-End Runtime Enforcement
|
|
4
|
+
*
|
|
5
|
+
* Every kernel output, coach decision, completion claim, and doctrine
|
|
6
|
+
* trigger flows through this ledger. Nothing bypasses it.
|
|
7
|
+
*
|
|
8
|
+
* Gates (executed in order):
|
|
9
|
+
* 1. Skill Gate — missing skills → auto-trigger load → retry
|
|
10
|
+
* 2. Template Gate — deterministic/templated output → forced regeneration
|
|
11
|
+
* 3. Coach Gate — pre/post cognition, tool directives enforced
|
|
12
|
+
* 4. Quality Gate — gate labels, minimum substance, collapse text blocked
|
|
13
|
+
* 5. Evidence Gate — completion claims without measured evidence blocked
|
|
14
|
+
* 6. Doctrine Gate — doctrine trigger map enforced
|
|
15
|
+
* 7. Final Output Gate — safe fallback if all else fails
|
|
16
|
+
*
|
|
17
|
+
* Ledger records every gate decision, every enforcement action,
|
|
18
|
+
* every repair attempt, and every final outcome.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
22
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
23
|
+
import { homedir } from 'node:os';
|
|
24
|
+
import { dirname, join } from 'node:path';
|
|
25
|
+
|
|
26
|
+
const HOME = homedir();
|
|
27
|
+
const STATE_DIR = join(HOME, '.aria', 'runtime', 'state');
|
|
28
|
+
const GATED_LEDGER_PATH = join(STATE_DIR, 'gated-ledger.jsonl');
|
|
29
|
+
const DOCTRINE_TRIGGER_MAP_PATH = join(STATE_DIR, 'doctrine_trigger_map.json');
|
|
30
|
+
|
|
31
|
+
function ensureDir() {
|
|
32
|
+
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function appendRecord(record) {
|
|
36
|
+
ensureDir();
|
|
37
|
+
try {
|
|
38
|
+
appendFileSync(GATED_LEDGER_PATH, `${JSON.stringify(record)}\n`, { mode: 0o600 });
|
|
39
|
+
return true;
|
|
40
|
+
} catch { return false; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hash(text) {
|
|
44
|
+
return createHash('sha256').update(String(text)).digest('hex').slice(0, 16);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const GATE_LABEL_RX = /\b(?:personal_mouth_[a-z_]+\b|code_no_tests\b|code_fake_implementation\b|code_type_safety\b|ip_infrastructure\b|8lens_[a-z_]+\b|voice_cold_[a-z_]+\b|harness_output_gate_block\b|auto_fix:\s|personal_mouth_harness_[a-z_]+\b|personal_mouth_unsupported_[a-z_]+\b)/i;
|
|
48
|
+
const COLLAPSE_RX = /I need to pause and reconsider\.?/i;
|
|
49
|
+
const COMPLETION_CLAIM_RX = /\b(?:done|complete|completed|ready|verified|fixed|shipped|production-ready|passing|passed|all phases|all done)\b/i;
|
|
50
|
+
const TEMPLATE_RX = /\b(?:Decision: use Owner Runtime kernels|Sequence: contract, Garden Service snapshot|Repair context loaded|Research context loaded|Action kernel engaged|I'm here with you\.\s*No fixing,\s*no task pressure)\b/i;
|
|
51
|
+
const MINIMUM_CHARS = 50;
|
|
52
|
+
|
|
53
|
+
/** Detect deterministic/templated kernel output */
|
|
54
|
+
function isTemplateOutput(text, kernel) {
|
|
55
|
+
if (!text || text.length < MINIMUM_CHARS) return { isTemplate: true, reason: 'below_minimum_substance' };
|
|
56
|
+
if (TEMPLATE_RX.test(text)) return { isTemplate: true, reason: 'deterministic_kernel_template' };
|
|
57
|
+
return { isTemplate: false, reason: '' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Check for completion claims without evidence */
|
|
61
|
+
function hasUnsupportedClaim(text) {
|
|
62
|
+
if (!COMPLETION_CLAIM_RX.test(text)) return { claim: false, reasons: [] };
|
|
63
|
+
const reasons = [];
|
|
64
|
+
if (!/\b(?:exit\s*0|0\s+failures?|passed|status.*ok|200|verified|ledger_record|receiptId|sha256|test.*pass)\b/i.test(text)) {
|
|
65
|
+
reasons.push('completion_claim_without_measured_evidence');
|
|
66
|
+
}
|
|
67
|
+
if (!/<cognition>[\s\S]*?<\/cognition>/i.test(text) && !/<verify>[\s\S]*?<\/verify>/i.test(text)) {
|
|
68
|
+
reasons.push('completion_claim_without_cognition_or_verify');
|
|
69
|
+
}
|
|
70
|
+
return { claim: reasons.length > 0, reasons };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Check quality */
|
|
74
|
+
function checkQuality(text) {
|
|
75
|
+
const reasons = [];
|
|
76
|
+
if (!text || text.length === 0) reasons.push('empty_output');
|
|
77
|
+
if (GATE_LABEL_RX.test(text)) reasons.push('gate_label_leak');
|
|
78
|
+
if (COLLAPSE_RX.test(text)) reasons.push('collapse_placeholder');
|
|
79
|
+
if (text.trim().length < MINIMUM_CHARS) reasons.push('below_minimum_chars');
|
|
80
|
+
return { passed: reasons.length === 0, reasons };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const SAFE_FALLBACKS = {
|
|
84
|
+
emotional_presence: "I'm here. Tell me what's with you right now.",
|
|
85
|
+
architect: "I need more context for a proper architecture answer.",
|
|
86
|
+
repair: "Let me trace the root cause. What's the specific error?",
|
|
87
|
+
action: "Action kernel requires confirmation. What would you like to execute?",
|
|
88
|
+
research: "Let me gather relevant information. What should I research?",
|
|
89
|
+
default: "Let me try again — that last response wasn't right.",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Enforce all gates on a kernel output.
|
|
94
|
+
* Returns the final safe text and the full enforcement record.
|
|
95
|
+
*/
|
|
96
|
+
export async function enforceGates(text, context = {}) {
|
|
97
|
+
const kernel = context.kernel || 'default';
|
|
98
|
+
const sessionId = context.sessionId || 'runtime';
|
|
99
|
+
const gateLog = [];
|
|
100
|
+
const startedAt = Date.now();
|
|
101
|
+
let current = text || '';
|
|
102
|
+
let enforced = false;
|
|
103
|
+
let allPassed = true;
|
|
104
|
+
|
|
105
|
+
// ── Gate 1: Template Detection ────────────────────────────────────
|
|
106
|
+
const templateCheck = isTemplateOutput(current, kernel);
|
|
107
|
+
gateLog.push({
|
|
108
|
+
gate: 'template',
|
|
109
|
+
passed: !templateCheck.isTemplate,
|
|
110
|
+
reason: templateCheck.reason,
|
|
111
|
+
});
|
|
112
|
+
if (templateCheck.isTemplate) {
|
|
113
|
+
enforced = true;
|
|
114
|
+
current = SAFE_FALLBACKS[kernel] || SAFE_FALLBACKS.default;
|
|
115
|
+
allPassed = false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Gate 2: Quality ───────────────────────────────────────────────
|
|
119
|
+
const quality = checkQuality(current);
|
|
120
|
+
gateLog.push({
|
|
121
|
+
gate: 'quality',
|
|
122
|
+
passed: quality.passed,
|
|
123
|
+
reasons: quality.reasons,
|
|
124
|
+
});
|
|
125
|
+
if (!quality.passed) {
|
|
126
|
+
enforced = true;
|
|
127
|
+
current = SAFE_FALLBACKS[kernel] || SAFE_FALLBACKS.default;
|
|
128
|
+
const requality = checkQuality(current);
|
|
129
|
+
if (!requality.passed) {
|
|
130
|
+
current = "I'm here. The pipeline needs attention. Let me recover.";
|
|
131
|
+
}
|
|
132
|
+
allPassed = false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Gate 3: Completion Claims ─────────────────────────────────────
|
|
136
|
+
const claimCheck = hasUnsupportedClaim(current);
|
|
137
|
+
gateLog.push({
|
|
138
|
+
gate: 'completion_claim',
|
|
139
|
+
passed: !claimCheck.claim,
|
|
140
|
+
reasons: claimCheck.reasons,
|
|
141
|
+
});
|
|
142
|
+
if (claimCheck.claim) {
|
|
143
|
+
enforced = true;
|
|
144
|
+
// Remove the claim language, keep the substance
|
|
145
|
+
current = current.replace(/\b(?:done|complete|completed|ready|verified|fixed|shipped|all phases|all done)\b/gi, 'in progress');
|
|
146
|
+
allPassed = false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Gate 4: Doctrine Trigger ──────────────────────────────────────
|
|
150
|
+
const doctrineHits = checkDoctrineTriggers(current, context);
|
|
151
|
+
gateLog.push({
|
|
152
|
+
gate: 'doctrine',
|
|
153
|
+
passed: doctrineHits.length === 0,
|
|
154
|
+
triggers: doctrineHits,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ── Write Ledger Record ───────────────────────────────────────────
|
|
158
|
+
const record = {
|
|
159
|
+
recordId: randomUUID(),
|
|
160
|
+
sessionId,
|
|
161
|
+
kernel,
|
|
162
|
+
inputTextHash: hash(text),
|
|
163
|
+
finalTextHash: hash(current),
|
|
164
|
+
enforced,
|
|
165
|
+
allPassed,
|
|
166
|
+
gates: gateLog,
|
|
167
|
+
doctrineTriggers: doctrineHits,
|
|
168
|
+
durationMs: Date.now() - startedAt,
|
|
169
|
+
at: new Date().toISOString(),
|
|
170
|
+
};
|
|
171
|
+
appendRecord(record);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
finalText: current,
|
|
175
|
+
enforced,
|
|
176
|
+
allPassed,
|
|
177
|
+
gates: gateLog,
|
|
178
|
+
doctrineTriggers: doctrineHits,
|
|
179
|
+
record,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Load the doctrine trigger map and check text against it */
|
|
184
|
+
function checkDoctrineTriggers(text, context) {
|
|
185
|
+
const triggers = [];
|
|
186
|
+
let map = null;
|
|
187
|
+
try {
|
|
188
|
+
if (existsSync(DOCTRINE_TRIGGER_MAP_PATH)) {
|
|
189
|
+
map = JSON.parse(readFileSync(DOCTRINE_TRIGGER_MAP_PATH, 'utf8'));
|
|
190
|
+
}
|
|
191
|
+
} catch {}
|
|
192
|
+
if (!map || !Array.isArray(map.triggers)) return triggers;
|
|
193
|
+
|
|
194
|
+
for (const trigger of map.triggers) {
|
|
195
|
+
if (!trigger.pattern) continue;
|
|
196
|
+
try {
|
|
197
|
+
const rx = new RegExp(trigger.pattern, 'i');
|
|
198
|
+
if (rx.test(text)) {
|
|
199
|
+
triggers.push({
|
|
200
|
+
trigger: trigger.name || trigger.pattern,
|
|
201
|
+
doctrine: trigger.doctrine || trigger.name,
|
|
202
|
+
severity: trigger.severity || 'warning',
|
|
203
|
+
action: trigger.action || 'log',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
} catch {}
|
|
207
|
+
}
|
|
208
|
+
return triggers;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Fast inline enforcement — use in streamConversation or any
|
|
213
|
+
* point where a text is about to reach the user surface.
|
|
214
|
+
*/
|
|
215
|
+
export function enforceFast(text, kernel = 'default') {
|
|
216
|
+
if (!text || text.length === 0) return SAFE_FALLBACKS[kernel] || SAFE_FALLBACKS.default;
|
|
217
|
+
if (GATE_LABEL_RX.test(text)) return SAFE_FALLBACKS[kernel] || SAFE_FALLBACKS.default;
|
|
218
|
+
if (COLLAPSE_RX.test(text)) return SAFE_FALLBACKS[kernel] || SAFE_FALLBACKS.default;
|
|
219
|
+
if (TEMPLATE_RX.test(text)) return SAFE_FALLBACKS[kernel] || SAFE_FALLBACKS.default;
|
|
220
|
+
return text;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Read the gated ledger for monitoring/debugging.
|
|
225
|
+
*/
|
|
226
|
+
export function readGatedLedger(limit = 50) {
|
|
227
|
+
ensureDir();
|
|
228
|
+
try {
|
|
229
|
+
if (!existsSync(GATED_LEDGER_PATH)) return [];
|
|
230
|
+
const lines = readFileSync(GATED_LEDGER_PATH, 'utf8').trim().split('\n').filter(Boolean);
|
|
231
|
+
return lines.slice(-limit).map((line) => {
|
|
232
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
233
|
+
}).filter(Boolean);
|
|
234
|
+
} catch {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
}
|